mailer

package module
v0.7.2 Latest Latest
Warning

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

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

README

Mailer

A stream processing engine in Go, inspired by Apache Flink.

Mailer reads unbounded data streams from sources like Apache Kafka, applies real-time transformations (map, filter, reduce, window), and writes results to sinks. It supports stateful processing, windowed aggregations, durable disk-backed state, and fault tolerance via checkpointing — up to end-to-end exactly-once for Kafka-to-Kafka pipelines.


Why?

Apache Flink is powerful but Java-heavy and complex. There's no idiomatic Go stream processing engine that lets you:

  • Consume Kafka topics as unbounded streams
  • Apply stateful transformations with exactly-once semantics
  • Window and aggregate events by time
  • Recover from failures without data loss

Mailer fills this gap. It's a lightweight, embeddable Go library — not a cluster runtime. You import it, define your pipeline, and run it.


Core Concepts

Stream

An unbounded, ordered sequence of records. A stream is created from a Source and flows through a chain of Operators.

Source[Kafka] → Map → Filter → Window(5min) → Reduce → Sink[Postgres]
Record

A single data item flowing through a stream. Every record carries:

Field Type Description
Key []byte Partition key (used for keyed state and shuffling)
Value []byte The actual data payload
Timestamp time.Time Event timestamp (for time-based operations)
Offset int64 Source offset (for checkpointing and replay)
Partition int Source partition (for barrier-aligned offset tracking)
Headers map[string][]byte Optional metadata headers
Source

Where data enters the pipeline. A Source reads from an external system and emits Records into the stream.

Source Description
KafkaSource Consumes from one or more Kafka topics with consumer group support
GeneratorSource Generates synthetic records for testing
SliceSource Reads from an in-memory slice (for testing)
Sink

Where results leave the pipeline. A Sink receives processed Records and writes them to an external system.

Sink Description
KafkaSink Produces to a Kafka topic (SASL/TLS, batch writes, serializer); at-least-once
TxnKafkaSink Transactional Kafka producer for end-to-end exactly-once (franz-go, per-checkpoint transactions)
PostgresSink Batch inserts/upserts into Postgres tables (pgx, retry, full mapper)
StdoutSink Prints records to stdout (debugging)
BlackholeSink Discards everything (benchmarking)
Operator

A transformation applied to a stream. Each operator takes one or more input streams and produces one or more output streams.

Operator Signature Description
Map func(Record) Record Transform each record 1:1
FlatMap func(Record) []Record Transform each record 1:many
Filter func(Record) bool Keep or drop records
KeyBy func(Record) []byte Partition stream by key (enables keyed state + parallelism)
Reduce func(accum []byte, curr Record) []byte Per-key aggregate: fold each record into a byte accumulator
Window WindowAssigner Group records into time-based windows
Process func(Record) (Record, error) Error-aware transform with failure policy (drop / DLQ / fail)
Keyed Stream

After KeyBy, the stream is partitioned by key. Each key gets its own state — this is how you do per-user counters, per-session windows, etc. State is always local to the key, no cross-key coordination needed.

stream.KeyBy(func(r types.Record) []byte { return r.Key }).WithPartitions(8).
       Reduce(func(accum []byte, curr types.Record) []byte { /* fold */ })
Window

Windows group an unbounded stream into finite chunks based on time. Without windows, aggregations like "count" or "sum" would never complete — the stream never ends.

Window Type Behavior Example
Tumbling Fixed-size, non-overlapping 5-minute tumbling: [0-5), [5-10), [10-15)
Sliding Fixed-size, overlapping with slide 5-min window sliding every 1 min
Session Gap-based, variable size Inactivity gap of 30 seconds

When a window closes, all records in that window are passed to the window function (Reduce, Process, etc.).

Watermark

A watermark is a timestamp that says "no records with timestamp < X will arrive after this point." Watermarks are how Mailer decides when a window is complete. If a record arrives after the watermark has passed its timestamp, it's late — and can be dropped or handled separately.

Records:    e1(2)  e2(5)  e3(8)  ---watermark(6)---  e4(7)  e5(10)
                                            ↑
                                    e3 is on-time
                                    e4 is late (7 < watermark 6)
                                    e5 is on-time

Watermarks are generated by the Source. For Kafka, a simple strategy is: watermark = max_timestamp_seen - allowed_lateness.

State

State is storage that persists across records (in RAM or on disk, depending on the backend). It's what makes a stream processor stateful.

State Type Use Case Example
ValueState Single value per key Current user score, Reduce accumulator
ListState Ordered list per key Recent login timestamps

State is stored in a State Backend, chosen per pipeline via WithStateBackend:

env.WithStateBackend(state.InMemory())          // default: RAM, serialized into checkpoints
env.WithStateBackend(state.Pebble("/var/lib/mailer/state")) // durable, disk-backed (LSM)

Each stateful operator instance (per keyed worker) gets its own isolated backend. With Pebble, checkpoints are native: the operator hard-links its LSM files at the barrier instead of serializing state, so checkpoint cost scales with changed data, not total state, and the checkpoint file stays small.

Measured at 5M keys (16-byte values): Pebble uses ~0.7 MB of heap vs 579 MB in-memory, checkpoints in ~75 ms vs ~3.3 s, and restores in ~58 ms vs ~2.9 s — at the cost of ~5.5 µs lookups (vs 0.3 µs) and ~25% pipeline throughput. Below ~100k keys the in-memory backend wins on everything except durability. Full numbers: go test -bench . -benchtime=1x ./bench/.

Checkpoint

A checkpoint is a consistent snapshot of:

  1. The current offset in each source partition
  2. The state of all operators

If the pipeline crashes, it restarts from the last successful checkpoint: sources rewind to the saved offsets, state is restored, and processing continues. Operator state is always exactly-once. Output is end-to-end exactly-once with TxnKafkaSink (each checkpoint interval's output commits in a Kafka transaction, atomically with the checkpoint) and at-least-once with all other sinks — see Delivery Guarantees below.

Checkpointing is based on the Chandy-Lamport algorithm (barriers flow through the stream, operators snapshot state when they see a barrier).

Source ──[record][record][barrier]──→ Map ──[barrier]──→ Sink
                                  ↓                    ↓
                            snapshot state        snapshot state
                            snapshot offset       ack checkpoint
Job

A Job is a complete pipeline definition: Source → Operator(s) → Sink. You submit a Job to the runtime, and it starts consuming, processing, and producing.


Architecture

┌──────────────────────────────────────────────────────┐
│                      Job                              │
│                                                      │
│  Source[Kafka]  →  Operator Chain  →  Sink[Postgres] │
│       │                  │                            │
│       │           ┌──────┴──────┐                     │
│       │           │   State     │                     │
│       │           │  Backend    │                     │
│       │           └──────┬──────┘                     │
│       │                  │                            │
│  ┌────┴──────────────────┴────┐                       │
│  │     Checkpoint Manager     │                       │
│  └────────────────────────────┘                       │
└──────────────────────────────────────────────────────┘

Single-process, multi-goroutine. No cluster runtime. Each source partition is consumed by a separate goroutine. Keyed state is partitioned by key across per-worker state backends (in-memory or Pebble). Checkpointing uses barrier alignment.

Execution Model: Stages and Edges

At Execute(), the planner groups the operator chain into execution stages. Inside a stage, operators run as direct function calls — no channels, no goroutine hops. Bounded channels (edges, default capacity 1024) exist only between stages:

[Source] →edge→ [Map→Filter] →edge→ [KeyBy: Window→Reduce ×N workers] →edge→ [Sink]
  • Consecutive stateless operators (Map, Filter, FlatMap, Process) with the same parallelism share one stage.
  • KeyBy starts a keyed stage: a router hash-dispatches records to N stateful workers (same key → same worker), each with cloned operators and isolated state.
  • Checkpoint barriers and watermarks are broadcast to every worker of a parallel stage and re-aligned at its exit, so snapshots stay consistent at any parallelism.

Backpressure falls out of the bounded edges: sending to a full edge blocks. A slow sink fills its input edge, which blocks the stage before it, and so on back to the source — which simply stops fetching from Kafka. Bounded memory, zero drops, no tuning required. Tune the buffer/latency trade-off with:

env := mailer.NewEnv().
    WithBufferSize(2048)          // edge capacity (default 1024)

stream.Map(cpuHeavyTransform).
    WithParallelism(4)            // worker pool for one stateless op
                                  // (order across workers not preserved)

Shutdown is two-phase: cancelling the context stops the source; everything downstream drains through cascading channel closes (a final checkpoint barrier rides the drain, so state is saved). Only if the drain exceeds WithShutdownTimeout (default 30s) are blocked stages forcibly aborted.

Observability: every edge and stage exports Prometheus metrics — mailer_edge_queue_size / _capacity (an edge pinned at capacity identifies the bottleneck stage right after it), mailer_stage_records_in_total / _out_total, mailer_stage_send_block_seconds_total (time spent blocked on backpressure), mailer_stage_workers, and mailer_stage_errors_total, alongside the existing per-operator and per-worker metrics.


SDK API

package main

import (
    "context"
    "time"

    "github.com/ASHUTOSH-SWAIN-GIT/mailer"
    "github.com/ASHUTOSH-SWAIN-GIT/mailer/sink"
    "github.com/ASHUTOSH-SWAIN-GIT/mailer/source"
    "github.com/ASHUTOSH-SWAIN-GIT/mailer/types"
    "github.com/ASHUTOSH-SWAIN-GIT/mailer/window"
)

func main() {
    env := mailer.NewEnv()

    kafkaSource := source.NewKafkaSource(
        source.KafkaBrokers("localhost:9092"),
        source.KafkaTopic("orders"),
        source.KafkaGroupID("order-processor"),
        source.KafkaStartFrom(source.OffsetEarliest),
        source.KafkaWithWatermarks(1 * time.Second),
    )

    kafkaSink := sink.NewKafkaSink(
        sink.KafkaSinkBrokers("localhost:9092"),
        sink.KafkaSinkTopic("order-summary"),
    )

    env.
        FromSource(kafkaSource).
        KeyBy(func(r types.Record) []byte { return r.Key }).WithPartitions(8).
        Window(window.NewTumbling(5 * time.Minute)).
        Reduce(func(accum []byte, curr types.Record) []byte {
            // accum is this key's running aggregate ([]byte, nil on
            // first record); return the updated aggregate.
            return addAmount(accum, curr)
        }).
        ToSink(kafkaSink)

    env.Execute(context.Background())
}
Fluent Chain
stream.
    Map(parseOrder).                        // transform 1:1
    Filter(isValidOrder).                   // drop invalid
    KeyBy(customerKey).WithPartitions(8).   // partition by customer, 8 keyed workers
    Window(window.NewTumbling(5 * time.Minute)).
    Reduce(aggregateAmount).                // per-key aggregate ([]byte accumulator)
    ToSink(kafkaSink)
Process (error-aware transform)

Process wraps a user function that may fail; the failure policy decides what happens to the record (drop it, send it to a dead-letter queue, or fail the pipeline):

stream.Process(func(r types.Record) (types.Record, error) {
    if !isValid(r) {
        return r, fmt.Errorf("invalid order")
    }
    return enrich(r), nil
},
    operator.WithProcessFailurePolicy(operator.ProcFailureDLQ),
    operator.WithProcessDLQ(dlqSink),
)

Keyed state (per-key accumulators, window contents) is managed by the engine inside Reduce and Window — there is no user-facing state API yet; a Flink-style stateful ProcessFunction with direct state access is on the roadmap.

Declarative Workflows

Common pipelines can also be defined in YAML/JSON and run without Go code. The declarative path supports built-in JSON-field filters, projection/rename/set, key-by-field keyed state, count/sum reduce, windows, sources, sinks, state, checkpointing, and environment-backed secrets.

go run ./cmd/mailer-workflow --file examples/workflows/order-totals.yaml
go run ./cmd/mailer-workflow --file examples/workflows/order-totals.yaml --dry-run --describe

Secrets use ${VAR} placeholders in sensitive fields:

sink:
  type: postgres
  postgres:
    dsn: ${POSTGRES_DSN}
    table: customer_totals
    mapping:
      customer.id: customer_id
      sum: total_amount
    mode: upsert
    conflictColumns: [customer_id]

The runner resolves those placeholders at compile time and does not include resolved values in summaries, errors, or pipeline descriptions.


Package Structure

mailer/
├── mailer.go              # StreamExecutionEnv, NewEnv(), Execute(), checkpointing glue
├── stream.go              # Stream type (fluent chain builder)
├── metadata.go            # Pipeline description for the dashboard
├── types/
│   └── record.go          # Record type (data / watermark / barrier)
├── pipeline/              # Stage-based execution engine
│   ├── stage.go           # Stage interface, SourceStage, SinkStage, ChannelStage
│   ├── edge.go            # Bounded edges + blocking sends (backpressure)
│   ├── planner.go         # Groups operators into execution stages
│   ├── stateless_stage.go # Chained stateless operators, worker pool
│   ├── keyed_stage.go     # Router → keyed workers → aligning merger
│   ├── markers.go         # Barrier/watermark broadcast + alignment
│   └── metrics.go         # Per-stage / per-edge instrumentation
├── operator/
│   ├── operator.go        # Operator + optional interfaces: SingleProcessor, Cloneable,
│   │                      #   Parallel, StateConfigurable, Snapshotable, BarrierSnapshotter
│   ├── map.go             # Map (1:1)
│   ├── flatmap.go         # FlatMap (1:N)
│   ├── filter.go          # Filter
│   ├── process.go         # Process (error-aware, failure policy + DLQ)
│   ├── keyby.go           # KeyBy router (hash partitioning)
│   ├── reduce.go          # Reduce (keyed accumulator)
│   └── window.go          # Window operator (buffers, fires on watermark)
├── window/                # Window assigners: tumbling, sliding, session
├── watermark/             # Watermark strategies (bounded out-of-orderness)
├── state/                 # StateBackend interface; in-memory + Pebble (LSM) backends,
│                          #   ValueState/ListState, native hard-link checkpoints
├── source/                # Source interface; Kafka (multi-partition, SASL/TLS,
│                          #   deserializers, watermarks), slice/generator for tests
├── sink/                  # Sink interface; Kafka, Postgres (batch+retry),
│                          #   stdout, blackhole; serializers, failure policies, DLQ
├── checkpoint/            # Barrier-based checkpoint data, file storage (fsync,
│                          #   status pointers), two-phase exactly-once coordinator
├── auth/                  # SASL / TLS config shared by Kafka source & sink
├── observability/
│   ├── metrics/           # Prometheus registry: pipeline, operator, stage, edge
│   └── dashboard/         # Built-in web dashboard
├── bench/                 # State-backend scaling benchmarks (memory vs Pebble)
├── examples/
│   ├── wordcount/         # FlatMap → KeyBy → Reduce
│   ├── windowing/         # Event time, watermarks, tumbling windows
│   ├── backpressure/      # Fast source + slow sink, bounded edges
│   ├── exactly-once/      # Kafka → keyed reduce → transactional Kafka
│   ├── kafka-orders/      # Kafka → Window → Kafka
│   ├── kafka-dashboard/   # Kafka pipeline with the live dashboard
│   ├── pg-orders/         # Kafka → Postgres
│   └── dashboard-demo/    # Metrics + dashboard
└── test/unit_tests/       # Integration tests: checkpoint recovery, exactly-once
                           #   crash sweep, backpressure, shutdown, durable state,
                           #   per-package unit tests

Implementation Status

All originally planned phases are implemented:

  • Core pipeline — Record, fluent Stream API, Map/Filter/FlatMap/Process, sources and sinks.
  • Stateful processing — per-key state, Reduce, Process with failure policies + DLQ.
  • Windowing & watermarks — tumbling/sliding/session windows, bounded out-of-orderness watermarks, late-record dropping.
  • Kafka & Postgres connectors — multi-partition Kafka source (consumer groups, SASL/TLS, deserializers, per-partition offset checkpointing), Kafka + Postgres sinks with batching, retries, serializers, and Postgres upserts.
  • Checkpointing & recovery — barrier-based snapshots, file storage, restore of operator state + per-partition source offsets on restart.
  • Keyed parallelismWithPartitions(n): router → N stateful workers with cloned operators and isolated state; barriers/watermarks broadcast and re-aligned so checkpoints stay consistent.
  • Stage-based execution & backpressure — operators grouped into stages, direct function-call chaining inside a stage, bounded edges between stages, WithParallelism(n) for stateless workers, two-phase graceful shutdown.
  • Observability — Prometheus metrics (pipeline, operator, worker, stage, edge) and a built-in dashboard.
  • End-to-end exactly-once (Kafka → Kafka) — coordinated two-phase checkpoints: barrier-aligned source offsets, synchronous operator snapshots at barrier passage, transactional sink (TxnKafkaSink on franz-go) with per-checkpoint transaction markers for crash recovery. See Delivery Guarantees.
  • Durable state backend (Pebble) — per-worker disk-backed LSM state selected via WithStateBackend; Reduce accumulators and Window records/watermark both live in the backend, so state is bounded by disk, not RAM. Native hard-link checkpoints make checkpoint cost scale with changed data, not total state. See State above.

Up next (roughly in order): allowed-lateness + side outputs for late data, multi-stream joins, typed Stream[T] API.


Key Design Decisions

Why single-process, not distributed?

Flink runs as a JobManager + TaskManager cluster. Mailer runs as a single Go process with goroutines. This makes it simple, embeddable, and easy to reason about. If you need multi-node parallelism, run multiple Mailer instances with different consumer group IDs (Kafka handles partitioning).

Why barrier-based checkpointing?

The Chandy-Lamport approach (barriers flow through the dataflow graph, operators snapshot on barrier arrival) is well-proven in Flink. Barriers are special Records that flow in-band with data; stateful operators snapshot synchronously as the barrier passes through them, and at parallel stages barriers are broadcast to every worker and strictly re-aligned at the stage exit — no record can overtake a pending barrier. A snapshot is therefore always a consistent cut of the stream.

Delivery Guarantees

Configuration Guarantee
KafkaSource(KafkaExactlyOnce()) → … → TxnKafkaSink + WithCheckpointing End-to-end exactly-once. Source offsets, operator state, and sink output commit as one coordinated checkpoint (two-phase commit; the checkpoint file is the transaction log; a per-checkpoint transaction marker resolves crashes between sink commit and checkpoint completion).
Any source → any plain sink, WithCheckpointing on Exactly-once state, at-least-once output: replay after recovery re-emits records the sink already wrote.
No checkpointing At-most-once across restarts (processing restarts from the source's configured start offset).

Requirements for the exactly-once configuration:

  • Consumers of the output topic must use isolation.level=read_committed — otherwise they observe records from aborted transactions and all guarantees are void.
  • The TxnKafkaTransactionalID must be stable across restarts and unique per pipeline instance (a second instance with the same ID fences the first).
  • The marker topic (<topic>.checkpoints by default) must not be deleted — it is how recovery proves whether an unconfirmed transaction committed.
  • Output visibility latency equals the checkpoint interval: records become readable when their interval's transaction commits.
  • A checkpoint failure fails the pipeline (the aborted transaction's output must be replayed); restart recovers from the last completed checkpoint.
Why not just use Kafka Streams?

Kafka Streams is Java-only. Mailer gives Go developers a native, embeddable stream processing library with similar semantics — without the JVM.

Why segmentio/kafka-go over confluent-kafka-go?

segmentio/kafka-go is pure Go (no CGO), which simplifies builds and cross-compilation. If performance becomes critical, we can add a confluent-kafka-go source as an alternative.


Feature Apache Flink Mailer
Language Java Go
Deployment Cluster (JobManager + TaskManagers) Single process, embeddable
API DataStream API, Table API, SQL DataStream API (fluent chain)
State Backends RocksDB, Memory Memory + Pebble (LSM), native hard-link checkpoints
Checkpointing Barrier-based, aligned/unaligned Barrier-based, aligned
Windowing Tumbling, Sliding, Session, Global Tumbling, Sliding, Session
Kafka Connector Built-in, mature Built-in (segmentio/kafka-go)
Exactly-once End-to-end (with transactional sinks) End-to-end (Kafka→Kafka via TxnKafkaSink); other sinks at-least-once
Backpressure Credit-based network flow control Bounded edges between stages
SQL Yes No (not planned for v1)
CEP (Complex Event Processing) Yes No (future)

Status

Actively developed. The engine (stage-based execution, keyed parallelism, checkpoint/recovery, backpressure, metrics) is implemented and covered by unit + integration tests, including crash-and-recovery and shutdown-under-load scenarios. See Implementation Status above for what's done and what's next.

Documentation

Index

Constants

View Source
const DefaultEdgeCapacity = 1024

DefaultEdgeCapacity is the default buffer size of the bounded edges between execution stages. Override with WithBufferSize.

Variables

This section is empty.

Functions

This section is empty.

Types

type CheckpointInfo

type CheckpointInfo struct {
	Interval string `json:"interval"`
	Storage  string `json:"storage"`
}

CheckpointInfo describes the checkpointing configuration.

type OperatorInfo

type OperatorInfo struct {
	Type   string            `json:"type"`
	Label  string            `json:"label,omitempty"`
	Config map[string]string `json:"config,omitempty"`
}

OperatorInfo describes a single operator in the pipeline chain.

type PipelineInfo

type PipelineInfo struct {
	Source     source.SourceInfo `json:"source"`
	Operators  []OperatorInfo    `json:"operators"`
	Sink       sink.SinkInfo     `json:"sink"`
	Checkpoint *CheckpointInfo   `json:"checkpoint,omitempty"`
}

PipelineInfo is the JSON-serializable description of a pipeline. It is returned by Describe() and consumed by the dashboard.

type Stream

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

Stream represents a pipeline stage. Method calls on Stream append operators to the chain and return the updated Stream.

Streams are built using a fluent API:

env.FromSource(src).Map(fn).Filter(fn).ToSink(s)

The pipeline is lazy — nothing runs until env.Execute() is called.

func (*Stream) Filter

func (s *Stream) Filter(fn func(types.Record) bool, label ...string) *Stream

Filter keeps only records where fn returns true. The optional label is shown in the dashboard.

func (*Stream) FlatMap

func (s *Stream) FlatMap(fn func(types.Record) []types.Record, label ...string) *Stream

FlatMap applies a 1:many transformation to each record. The function returns a slice; if empty, the record is dropped. The optional label is shown in the dashboard.

func (*Stream) KeyBy

func (s *Stream) KeyBy(fn operator.KeySelector, label ...string) *Stream

KeyBy partitions the stream by the given key selector function. All records with the same key are routed to the same keyed worker. Required before stateful operations like Window and Reduce.

Use WithPartitions to control the number of keyed workers:

stream.KeyBy(fn, "by-customer").WithPartitions(8).Window(...).Reduce(...)

func (*Stream) Map

func (s *Stream) Map(fn func(types.Record) types.Record, label ...string) *Stream

Map applies a 1:1 transformation to each record in the stream. If the function returns the zero value of Record, the record is dropped. The optional label is shown in the dashboard.

func (*Stream) Process added in v0.4.0

func (s *Stream) Process(fn func(types.Record) (types.Record, error), opts ...operator.ProcessOption) *Stream

Process applies a user function that may return an error. On error the failure policy (Drop / DLQ / Fail) is applied. Use the operator.WithProcessFailurePolicy and operator.WithProcessDLQ options to configure failure handling.

stream.Process(func(r types.Record) (types.Record, error) {
    if !isValid(r) { return r, fmt.Errorf("invalid") }
    return enrich(r), nil
}, operator.WithProcessFailurePolicy(operator.ProcFailureDLQ)).
    ToSink(s)

func (*Stream) Reduce

func (s *Stream) Reduce(fn operator.ReduceFn, label ...string) *Stream

Reduce applies a stateful aggregation per key. Must be used after KeyBy. The reduce function is called with the current accumulator (nil on first call) and the incoming record, and returns the new accumulator. The updated accumulator is emitted downstream after every record.

Example (count per key):

stream.KeyBy(func(r types.Record) []byte { return r.Key }).
    Reduce(func(accum []byte, curr types.Record) []byte {
        count := 0
        if accum != nil {
            count = int(binary.BigEndian.Uint64(accum))
        }
        count++
        buf := make([]byte, 8)
        binary.BigEndian.PutUint64(buf, uint64(count))
        return buf
    })

func (*Stream) ToSink

func (st *Stream) ToSink(sk sink.Sink) *StreamExecutionEnv

ToSink connects the stream to a sink and returns the execution environment. The pipeline is still lazy — call env.Execute() to start processing.

func (*Stream) Window

func (s *Stream) Window(assigner window.WindowAssigner, label ...string) *Stream

Window groups records into time-based windows. Must be used after KeyBy. Records are buffered into windows, and when a watermark passes a window's end time, the window fires — all its records are emitted as a single result.

Supported window types:

  • window.Tumbling(size): fixed-size, non-overlapping (e.g. 5-minute buckets)
  • window.Sliding(size, slide): overlapping windows (e.g. 5-min every 1-min)
  • window.Session(gap): variable-size, closes after inactivity gap

Example (5-minute tumbling window):

stream.KeyBy(func(r types.Record) []byte { return r.Key }).
    Window(window.Tumbling(5 * time.Minute)).
    Reduce(aggregateFn)

func (*Stream) WindowWithIdleTimeout

func (s *Stream) WindowWithIdleTimeout(assigner window.WindowAssigner, idleTimeout time.Duration, label ...string) *Stream

WindowWithIdleTimeout creates a window with an idle timeout. If no records arrive within the timeout duration, all remaining windows are fired and the pipeline stage completes. Useful for infinite streams that don't receive shutdown signals.

func (*Stream) WithParallelism added in v0.4.0

func (s *Stream) WithParallelism(n int) *Stream

WithParallelism sets the number of workers for the most recently added stateless operator (Map, Filter, FlatMap, Process). Must be called directly after the operator it configures.

Consecutive operators with the same parallelism share one execution stage. Note: with parallelism > 1, record order is NOT preserved across workers — use it for CPU-heavy, order-insensitive transforms.

func (*Stream) WithPartitions added in v0.4.0

func (s *Stream) WithPartitions(n int) *Stream

WithPartitions sets the number of keyed workers for the most recently added KeyBy operator. Must be called directly after KeyBy.

type StreamExecutionEnv

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

StreamExecutionEnv is the entry point for building and running stream pipelines. Create one with NewEnv(), define your pipeline using FromSource/ToSink, then call Execute() to run it.

env := mailer.NewEnv()
env.FromSource(src).Map(fn).Filter(fn).ToSink(stdout)
env.Execute(ctx)

func NewEnv

func NewEnv() *StreamExecutionEnv

NewEnv creates a new StreamExecutionEnv.

func (*StreamExecutionEnv) Describe

func (env *StreamExecutionEnv) Describe() PipelineInfo

Describe returns a serializable description of the pipeline: source, operator chain, sink, and checkpoint config. Call this before Execute() to get the pipeline graph for the dashboard.

func (*StreamExecutionEnv) DescribeJSON

func (env *StreamExecutionEnv) DescribeJSON() string

DescribeJSON returns the pipeline description as indented JSON.

func (*StreamExecutionEnv) Execute

func (env *StreamExecutionEnv) Execute(ctx context.Context) error

Execute runs the pipeline. Operators are grouped into execution stages (see the pipeline package); stages run concurrently, connected by bounded edges. A full edge blocks the upstream stage — backpressure propagates stage by stage back to the source, so a slow sink throttles ingestion instead of growing memory.

Graceful shutdown (on context cancellation, C3 two-phase):

  1. The source stops producing and flushes pending offset commits.
  2. A final checkpoint barrier is injected (if checkpointing is on).
  3. Channel closes cascade downstream; every stage drains in-flight records — nothing is dropped.
  4. The sink drains and the final checkpoint is saved.
  5. Only if draining exceeds shutdownTimeout are blocked stages forcibly aborted.

Prometheus metrics are collected automatically during execution.

func (*StreamExecutionEnv) FromSource

func (env *StreamExecutionEnv) FromSource(src source.Source) *Stream

FromSource sets the data source for the pipeline and returns a Stream that you can chain operators on.

func (*StreamExecutionEnv) WithBufferSize added in v0.4.0

func (env *StreamExecutionEnv) WithBufferSize(n int) *StreamExecutionEnv

WithBufferSize sets the capacity of the bounded edges between execution stages (default 1024). Larger buffers absorb bursts; smaller buffers propagate backpressure to the source sooner. Values < 1 are ignored.

func (*StreamExecutionEnv) WithCheckpointHook added in v0.4.0

func (env *StreamExecutionEnv) WithCheckpointHook(fn func(checkpoint.Step, string) checkpoint.HookAction) *StreamExecutionEnv

WithCheckpointHook installs a test-only hook fired after each coordinated checkpoint protocol step. Used by crash-window tests to halt or fail the coordinator at exact positions. Not for production.

func (*StreamExecutionEnv) WithCheckpointListener added in v0.5.0

func (env *StreamExecutionEnv) WithCheckpointListener(fn func(id string)) *StreamExecutionEnv

WithCheckpointListener registers an observer called with the checkpoint ID whenever a checkpoint completes — covering both the uncoordinated (at-least-once) and coordinated (exactly-once) paths. It is a progress signal for supervisors such as the job agent; the callback must not block and must not mutate pipeline state.

func (*StreamExecutionEnv) WithCheckpointing

func (env *StreamExecutionEnv) WithCheckpointing(interval time.Duration, storage checkpoint.Storage) *StreamExecutionEnv

WithCheckpointing enables periodic checkpointing with the given interval and storage backend. Barriers are injected into the stream at the specified interval; when a barrier passes through all operators and reaches the sink, the checkpoint is complete.

On recovery, Execute() will load the latest checkpoint, restore all stateful operators, and resume from the saved source offset.

Example:

env := mailer.NewEnv()
env.WithCheckpointing(30*time.Second, checkpoint.NewFileStorage("/tmp/checkpoints"))

func (*StreamExecutionEnv) WithShutdownTimeout added in v0.4.0

func (env *StreamExecutionEnv) WithShutdownTimeout(d time.Duration) *StreamExecutionEnv

WithShutdownTimeout sets how long the pipeline waits for in-flight records to drain before forcing shutdown (default 30s).

func (*StreamExecutionEnv) WithStateBackend added in v0.4.0

func (env *StreamExecutionEnv) WithStateBackend(f state.BackendFactory) *StreamExecutionEnv

WithStateBackend sets the factory used to create state backends for stateful operators (Reduce, and any operator implementing operator.StateConfigurable). The factory is called once per state owner while the execution plan is built — "op-<i>" for top-level operators, "worker-<idx>" for keyed-worker clones — so every owner gets isolated state. Backends implementing io.Closer are closed when Execute returns.

Default: state.InMemory() semantics (each operator keeps its own in-memory backend).

Directories

Path Synopsis
cmd
mailer-runner command
Command mailer-runner is the generic job entrypoint baked into the runner container image.
Command mailer-runner is the generic job entrypoint baked into the runner container image.
mailer-workflow command
control module
examples
backpressure command
Backpressure demo: a fast source feeding a deliberately slow sink through tiny edges.
Backpressure demo: a fast source feeding a deliberately slow sink through tiny edges.
exactly-once command
Exactly-once pipeline: Kafka → keyed aggregation → transactional Kafka.
Exactly-once pipeline: Kafka → keyed aggregation → transactional Kafka.
kafka-orders command
pg-orders command
windowing command
wordcount command
Package jobagent supervises a single Mailer job in-process and exposes its lifecycle over HTTP.
Package jobagent supervises a single Mailer job in-process and exposes its lifecycle over HTTP.
observability
Package pipeline implements stage-based execution for stream pipelines.
Package pipeline implements stage-based execution for stream pipelines.
Package sdk is the harness for SDK (Go) jobs run through the mailer control plane.
Package sdk is the harness for SDK (Go) jobs run through the mailer control plane.
Package state provides keyed state storage for stateful stream processing.
Package state provides keyed state storage for stateful stream processing.
Package watermark provides watermark generation for event-time processing.
Package watermark provides watermark generation for event-time processing.
Package window provides window assigners for grouping unbounded streams into finite chunks based on time.
Package window provides window assigners for grouping unbounded streams into finite chunks based on time.
Package workflow defines a declarative format for Mailer pipelines.
Package workflow defines a declarative format for Mailer pipelines.
compiler
Package compiler turns a parsed, validated workflow spec into the existing Mailer SDK objects (sources, operators, sinks, environment).
Package compiler turns a parsed, validated workflow spec into the existing Mailer SDK objects (sources, operators, sinks, environment).
operators
Package operators provides built-in stateless operators for declarative workflows.
Package operators provides built-in stateless operators for declarative workflows.
record
Package record provides a structured JSON view of a types.Record so declarative, field-level operations (map/filter/project by field name) can read and modify record contents safely.
Package record provides a structured JSON view of a types.Record so declarative, field-level operations (map/filter/project by field name) can read and modify record contents safely.

Jump to

Keyboard shortcuts

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