core

package
v1.0.6 Latest Latest
Warning

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

Go to latest
Published: Sep 6, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func InitCommands

func InitCommands(conn adbc.Connection, c *config.Conf) error

func InitTables

func InitTables(conn adbc.Connection, c *config.Conf) error

InitTables creates the tables that live across the pipeline's lifetime, such as the aggregate tables a tumbling window manager maintains.

func InitUDFs

func InitUDFs(c *config.Conf) error

InitUDFs reports that a config declares UDFs, which turbine does not support. The Python engine registers arbitrary Python callables through conn.create_function; Go has no equivalent, and rather than inventing a plugin mechanism, user-defined functions are left to DuckDB itself (a macro, an extension, or an ATTACHed database that defines them).

This is an error rather than a silent skip: ignoring the block would defer the failure to an unhelpful binder error when the handler SQL calls the function.

func OpenState added in v1.0.5

func OpenState(ctx context.Context, path string) (*duckdb.DB, error)

OpenState opens the DuckDB database backing a pipeline's durable state.

It exists to separate two cases that duckdb.OpenPath cannot tell apart. A path with no file is the first run, and creating it is correct. A path that holds something DuckDB refuses to open is a damaged state file, and starting over would replay from the beginning while reporting healthy.

A damaged file is never repaired, moved, or truncated here. It holds the only copy of the pipeline's positions, so it is evidence an operator needs.

Types

type ErrorPolicy

type ErrorPolicy int
const (
	PolicyRaise ErrorPolicy = iota
	PolicyIgnore
	PolicyDLQ
)

func ParseErrorPolicy

func ParseErrorPolicy(s string) (ErrorPolicy, error)

ParseErrorPolicy resolves the configured policy name. Matching the Python engine, the name is case-insensitive and an empty value means RAISE.

type Handler

type Handler interface {
	Init(ctx context.Context) error
	Write(msg []byte) error
	Invoke(ctx context.Context) (arrow.Table, error)
}

type Mark added in v1.0.4

type Mark struct {
	Offset      int64
	LeaderEpoch int32
}

Mark is the position of the last message the pipeline has finished with in one partition: written to the handler, or dropped by an error policy.

type MarkCommitter added in v1.0.4

type MarkCommitter interface {
	CommitMarks(marks *Marks) error
}

MarkCommitter is implemented by sources that can commit an explicit position. The pipeline prefers it to Commit, because a source that reads ahead of the pipeline -- the Kafka source polls into a buffer -- has fetched messages the pipeline has not processed, and committing "everything fetched" commits those too.

type Marks added in v1.0.5

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

Marks records the last position the pipeline has finished with in each partition it has read: a message written to the handler, or one dropped by an error policy.

Topic-then-partition nesting is the natural shape for this and the awkward one to pass around. Owning it here keeps two rules in one place rather than repeated at every call site: a partition only ever moves forward, and iteration is ordered so anything rendered from it -- the stats endpoint, the CLI -- is stable between runs.

func NewMarks added in v1.0.5

func NewMarks() *Marks

func (*Marks) Advance added in v1.0.5

func (m *Marks) Advance(topic string, partition int32, mark Mark)

Advance records a position, ignoring one that would move a partition backwards. Offsets arrive in order within a partition, so the guard only matters if a source redelivers -- and a redelivery must not rewind a position that has already been committed.

func (*Marks) Each added in v1.0.5

func (m *Marks) Each(fn func(topic string, partition int32, mark Mark))

Each calls fn for every position, ordered by topic then partition. The order is deliberate: map iteration is random, and callers render these into JSON and log lines where a shuffling order makes diffs unreadable.

func (*Marks) Empty added in v1.0.5

func (m *Marks) Empty() bool

func (*Marks) Get added in v1.0.5

func (m *Marks) Get(topic string, partition int32) (Mark, bool)

Get reports the position recorded for a partition. The boolean distinguishes "no position yet" from a position at offset zero, which is a real place in the log.

func (*Marks) Len added in v1.0.5

func (m *Marks) Len() int

Len is the number of partitions held, across all topics.

type Message

type Message struct {
	Value     []byte
	Topic     string
	Partition int32
	Offset    int64
	// LeaderEpoch is the Kafka leader epoch the record was read under. It is
	// carried through so a commit can name it, which lets the broker detect
	// log truncation. Only meaningful when HasMetadata is true; a source with
	// no positions leaves it zero along with the rest.
	LeaderEpoch int32
	// HighWatermark is the partition's high watermark at fetch time, so lag
	// can be computed against the position last processed. Zero for sources
	// without one.
	HighWatermark int64
}

Message is one record from a source, with whatever provenance the source knows about it. Only Kafka populates the metadata fields.

func (Message) HasMetadata

func (m Message) HasMetadata() bool

HasMetadata reports whether the source supplied provenance for this message. Topic is the discriminator: a Kafka record always has one, and a source that has none leaves it empty.

type MetadataWriter

type MetadataWriter interface {
	WriteMessage(msg Message) error
}

MetadataWriter is implemented by handlers that can use a message's source metadata. Handlers that only need the payload implement Handler alone and the consume loop hands them the value.

type Metrics

type Metrics struct {
	MessageCount           metric.Int64Counter
	ErrorCount             metric.Int64Counter
	SourceReadLatency      metric.Float64Histogram
	SinkFlushLatency       metric.Float64Histogram
	SinkFlushNumRows       metric.Int64Gauge
	SinkFlushCount         metric.Int64Counter
	BatchProcessingLatency metric.Float64Histogram
	StateCommitLatency     metric.Float64Histogram
	StateCommitCount       metric.Int64Counter
	StateSizeBytes         metric.Int64Gauge
	StateTableRows         metric.Int64Gauge
	ConsumerLag            metric.Int64Gauge
}

Metrics holds the instruments the pipeline records, mirroring the names, descriptions and units the Python engine exports.

func NewMetrics

func NewMetrics(mp metric.MeterProvider) (*Metrics, error)

NewMetrics builds the instruments from a meter provider. Passing a noop provider yields instruments that record nothing, so the pipeline needs no nil checks.

type OffsetStat added in v1.0.5

type OffsetStat struct {
	Topic       string `json:"topic"`
	Partition   int32  `json:"partition"`
	Offset      int64  `json:"offset"`
	LeaderEpoch int32  `json:"leader_epoch"`
}

OffsetStat is one partition's durable position.

type OffsetStore added in v1.0.5

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

OffsetStore persists Marks to DuckDB. It holds no lock and starts no transaction of its own: Save issues writes on the connection it is given, and it is the caller's job to commit -- typically together with the batch that advanced those offsets.

func NewOffsetStore added in v1.0.5

func NewOffsetStore(conn adbc.Connection) *OffsetStore

NewOffsetStore wraps a DuckDB connection. The connection is expected to be the same one the pipeline uses for its batch writes, so offsets land in the same transaction as the state they describe.

func (*OffsetStore) Init added in v1.0.5

func (s *OffsetStore) Init(ctx context.Context) error

Init prepares the offsets table, and refuses to run against a state file it cannot read.

The table is created only when it is absent, which is the first run. When it is present, its schema has to match offsetsSchema exactly. A table that carries the right name and the wrong shape means the file was written by something else, or damaged; either way the positions in it cannot be trusted.

The alternative -- CREATE TABLE IF NOT EXISTS, as this did before -- accepts the damaged table, finds no readable positions, and replays the topic from the beginning while reporting healthy. A silent restart from zero is worse than a refusal to start, so this returns CodeStateCorrupt and changes nothing on disk.

func (*OffsetStore) Load added in v1.0.5

func (s *OffsetStore) Load(ctx context.Context) (*Marks, error)

Load returns every position recorded in the offsets table. A store with no rows yet returns an empty, non-nil Marks -- the caller must not treat that as offset zero.

func (*OffsetStore) Save added in v1.0.5

func (s *OffsetStore) Save(ctx context.Context, marks *Marks) error

Save upserts one row per topic/partition in marks. It does not commit: the caller owns the transaction, so these writes can land together with whatever state change they are recording the position for.

type PipelineErrorPolicies

type PipelineErrorPolicies struct {
	Policy  ErrorPolicy
	DLQSink Sink
}

type Sink

type Sink interface {
	WriteTable(ctx context.Context, batch arrow.Table) error
	Flush(ctx context.Context) error
	Batch() (arrow.Table, error)
}

Sink is where a batch leaves the pipeline.

Both write paths take a context so a sink that retries can be interrupted. Without it, a retry ladder outlives the SIGTERM that asked the pipeline to stop, and the graceful drain waits out the full retry deadline before it can finish. See #110.

type Source

type Source interface {
	Start() error
	Stream() <-chan []Message
	Commit() error
	Close() error
}

type StateStats added in v1.0.5

type StateStats struct {
	Path      string       `json:"path"`
	SizeBytes int64        `json:"size_bytes"`
	Tables    []TableStat  `json:"tables"`
	Offsets   []OffsetStat `json:"offsets"`
}

StateStats is a snapshot of a pipeline's durable state: how large it is, what is in it, and how far through the stream it has committed.

It is produced once and rendered by two consumers -- the /stats endpoint of a running pipeline and the CLI reading a stopped one -- so the two can never disagree about what a pipeline's state contains.

func CollectStateStats added in v1.0.5

func CollectStateStats(ctx context.Context, conn adbc.Connection, path string) (*StateStats, error)

CollectStateStats reads a snapshot of the state database.

The connection should be one dedicated to reading, not the connection the pipeline writes on. Connections to a DuckDB database have independent transaction state, so a reader sees committed rows only: the numbers here describe what would survive a crash at this moment, and collecting them cannot block the writer or be blocked by it.

type Stats

type Stats struct {
	StartTime time.Time
	NumErrors int
	// contains filtered or unexported fields
}

func (*Stats) AddMessagesConsumed

func (s *Stats) AddMessagesConsumed(n int64)

func (*Stats) GetThroughput

func (s *Stats) GetThroughput() float64

func (*Stats) MessagesConsumed

func (s *Stats) MessagesConsumed() int64

func (*Stats) SetNumMessagesConsumed

func (s *Stats) SetNumMessagesConsumed(num int64)

func (*Stats) SetThroughput

func (s *Stats) SetThroughput(throughput float64)

type TableStat added in v1.0.5

type TableStat struct {
	Name string `json:"name"`
	Rows int64  `json:"rows"`
}

TableStat is one managed table and how much is in it.

type Turbine

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

func NewTurbine

func NewTurbine(
	source Source,
	handler Handler,
	sink Sink,
	batchSize int,
	flushInterval time.Duration,
	lock *sync.Mutex,
	policy PipelineErrorPolicies,
	opts ...TurbineOption,
) *Turbine

func (*Turbine) ConsumeLoop

func (t *Turbine) ConsumeLoop(ctx context.Context, maxMsgs int) (stats *Stats, err error)

func (*Turbine) StatusLoop

func (t *Turbine) StatusLoop(ctx context.Context) error

func (*Turbine) SyncState added in v1.0.5

func (t *Turbine) SyncState(ctx context.Context) error

SyncState closes the open state transaction, making everything written since the last commit durable. A pipeline with no state database does nothing.

Shutdown uses it twice: once before the table managers run their final poll, so that poll sees a current clock rather than one frozen at the last batch, and once after, so the rows that poll published are actually deleted instead of being rolled back when the connection closes and republished on the next start.

type TurbineOption

type TurbineOption func(turbine *Turbine)

func WithMetrics

func WithMetrics(m *Metrics) TurbineOption

WithMetrics records pipeline instruments through the given provider.

func WithStateStats added in v1.0.5

func WithStateStats(fn func() (*StateStats, error)) TurbineOption

WithStateStats supplies the snapshot function backing the state gauges. It must read a connection dedicated to reading; passing the pipeline's writer would let a scrape contend with batch processing.

func WithStateStore added in v1.0.5

func WithStateStore(offsets offsetSaver, tx stateTx) TurbineOption

WithStateStore makes each batch transactional: the handler's writes to the state database and the offsets that produced them commit together, so a crash can never leave one without the other.

func WithTurbineLogger

func WithTurbineLogger(l *zap.Logger) TurbineOption

Jump to

Keyboard shortcuts

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