distributed

package
v0.12.0-clickbench Latest Latest
Warning

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

Go to latest
Published: Aug 16, 2026 License: AGPL-3.0 Imports: 27 Imported by: 0

Documentation

Overview

Package distributed provides NATS-based distributed coordination.

Index

Constants

View Source
const (
	// Task subjects — JetStream WorkQueue retention
	SubjectTasksScan      = "wadjet.tasks.scan"
	SubjectTasksAggregate = "wadjet.tasks.aggregate"
	SubjectTasksJoin      = "wadjet.tasks.join"
	SubjectTasksSort      = "wadjet.tasks.sort"
	SubjectTasksWindow    = "wadjet.tasks.window"
	SubjectTasksShuffle   = "wadjet.tasks.shuffle"
	SubjectTasksAll       = "wadjet.tasks.>"

	// Result notifications — JetStream Interest retention
	SubjectResults    = "wadjet.results"
	SubjectResultsAll = "wadjet.results.>"

	// Worker heartbeats — Core NATS
	SubjectHeartbeat = "wadjet.workers.heartbeat"

	// Per-task progress — Core NATS. Published by workers from inside
	// task hot loops (every ~2s when progress is being made). Coord
	// subscribes via SubjectTaskProgressAll and feeds into per-stage
	// progress detection so a long-running task that's still pushing
	// rows is distinguishable from a wedged task that's stopped making
	// forward progress.
	SubjectTaskProgress    = "wadjet.task.progress"
	SubjectTaskProgressAll = "wadjet.task.progress.>"

	// Async-upload completion (streaming exchange Phase B) — Core NATS.
	// Published by workers when a task's background stage-output uploads
	// land; coordinator flips per-key durability bits. Best-effort: a
	// lost message leaves keys non-durable, which only means the
	// coordinator stays conservative about them.
	SubjectUploadComplete = "wadjet.uploads.complete"

	// Deferred-upload release (shuffle durability "lazy",
	// docs/design/shuffle-durability.md) — Core NATS. Published by the
	// coordinator when a root query's scratch needs its durable S3 copies
	// after all (a consumer reported a missing input whose producer is
	// still alive, or the coordinator itself needs to read a stage
	// output). Workers holding queued lazy uploads for that root start
	// them on receipt. Best-effort: a lost message costs one task-retry
	// round (the retry re-triggers the release), never correctness.
	SubjectUploadRelease = "wadjet.uploads.release"

	// Eager consumer dispatch (docs/design/eager-consumer-dispatch.md) —
	// Core NATS. Coordinator republishes a compact per-producer-task file
	// manifest as each task of an eager edge completes; consumer tasks'
	// manifest sources subscribe per (root query, producer stage).
	// Metadata-only; payload bytes never flow through these subjects.
	SubjectEagerManifest = "wadjet.eager"

	// Query cancellation — Core NATS
	SubjectCancel = "wadjet.cancel"

	// Query completion — Core NATS. Published by the coordinator after a
	// query finishes (success, failure, or cancellation) so workers can
	// release per-query resources (LocalStageCache spill files, etc).
	// Distinct from CancelSubject: CancelSubject still means "stop running
	// new tasks for this query"; CompleteSubject means "the query is done,
	// you may free its caches."
	SubjectComplete = "wadjet.complete"

	// Catalog locks — NATS KV
	SubjectCatalogLock = "wadjet.catalog.lock"

	// Query active check — Core NATS request/reply
	// Workers ask coordinator if a query is still active before executing
	// stale tasks pulled from JetStream.
	SubjectQueryActive = "wadjet.query.active"

	// Worker profile collection — Core NATS request/reply
	SubjectProfileStart   = "wadjet.workers.profile.start"
	SubjectProfileCollect = "wadjet.workers.profile.collect"

	// Dead-letter queue — failed tasks for inspection/retry
	SubjectDLQ    = "wadjet.dlq"
	SubjectDLQAll = "wadjet.dlq.>"

	// Priority task lane — latency-critical, dimension-class tasks
	// (dyn-filter emitter scans) that must never queue behind bulk scan
	// fan-out. Separate subject space + stream because WADJET_TASKS is a
	// WorkQueue stream whose main consumer filters wadjet.tasks.> — a
	// second consumer under the same prefix would overlap, which WorkQueue
	// retention forbids. Workers drain this lane with dedicated slots
	// outside MaxConcurrent (docs/design/attach-on-arrival-dynamic-filters.md).
	//
	// The lane is CLASS-SPLIT into two non-overlapping subject spaces:
	// "leaf" (emitters with no consumes — dims) and "deep" (guarded
	// re-emitters, which may block at finalize waiting on a leaf's bloom).
	// Each class gets its own WorkQueue consumer + worker slot pool so a
	// blocked deep task can never starve the leaf it waits on.
	SubjectPriTasksAll  = "wadjet.pritasks.>"
	SubjectPriTasksLeaf = "wadjet.pritasks.leaf.>"
	SubjectPriTasksDeep = "wadjet.pritasks.deep.>"

	// Stream names
	StreamTasks    = "WADJET_TASKS"
	StreamPriTasks = "WADJET_TASKS_PRI"
	StreamResults  = "WADJET_RESULTS"
	StreamDLQ      = "WADJET_DLQ"

	// KV bucket for catalog locks
	KVCatalogLocks = "wadjet_catalog_locks"
)

NATS subject hierarchy for Wadjet.

View Source
const BaseTablePeerKeyPrefix = "basetable:"

BaseTablePeerKeyPrefix marks a PeerExchange FetchShuffle key as a base-table cache fetch ("basetable:<bucket>/<object key>") rather than a query-scratch shuffle file. The prefix can never collide with scratch keys (those start "queries/") or bucket names (":" is not legal in S3 bucket names).

View Source
const (
	// KVUserFunctions is the NATS KV bucket for user-defined functions.
	KVUserFunctions = "wadjet_user_functions"
)

Variables

This section is empty.

Functions

func AffinityOwner

func AffinityOwner(file string, workers []string) string

AffinityOwner returns the rendezvous (highest-random-weight) owner of file among workers: argmax_w fnv64(file, w). Deterministic for a given worker set; a joining/leaving worker remaps only the files it wins/held.

This single definition is the contract between the coordinator's scan fan-out placement (scan_affinity.go) and the worker's base-table peer tier (base_table_peer.go): both sides must hash the same file string — the bare object key, no bucket prefix — over the same sorted domain of live, non-draining worker IDs, or a non-owner will dial the wrong peer and pay a harmless NotFound → S3 fallthrough.

func AffinityRank

func AffinityRank(file string, workers []string) []string

AffinityRank returns workers ordered by descending rendezvous weight for file: rank[0] is the AffinityOwner, rank[1] the runner-up, and so on. The runner-up is the file's stable secondary home — it inherits the file on owner departure, so byte-balance shedding (coordinator scan_affinity.go) targets it to keep shed placement consistent across stages, queries, and membership churn.

func BaseTablePeerKey

func BaseTablePeerKey(bucket, key string) string

BaseTablePeerKey encodes a (bucket, key) pair for a peer fetch.

func BuildNATSClientTLS

func BuildNATSClientTLS(certFile, keyFile, caFile string) (*tls.Config, error)

BuildNATSClientTLS creates a TLS config for a NATS client (worker connecting to coordinator). certFile/keyFile are the client certificate and key; caFile is the CA that signed the server cert.

func CancelSubject

func CancelSubject(queryID string) string

CancelSubject returns the NATS subject for cancelling a specific query.

func CancelSubjectAll

func CancelSubjectAll() string

CancelSubjectAll returns the wildcard subject for all cancellation messages.

func ClusterPriTaskSubject

func ClusterPriTaskSubject(clusterID, class, taskType, queryID, stageID string) string

ClusterPriTaskSubject is ClusterTaskSubject's counterpart on the priority lane. The CLASS token comes BEFORE the cluster ID — a non-cluster worker filters "wadjet.pritasks.<class>.>", and nesting the cluster under the class keeps that filter matching cluster-tagged subjects too (the same property the pre-split "wadjet.pritasks.>" filter had; token order the other way silently strands cluster-tagged tasks on class filters — caught by TestDistributedTPCH/Q07 hanging, 2026-08-07).

func ClusterPriTasksFilter

func ClusterPriTasksFilter(clusterID, class string) string

ClusterPriTasksFilter returns the priority-lane filter subject for a worker to receive only its cluster's priority tasks of one class.

func ClusterTaskSubject

func ClusterTaskSubject(clusterID, taskType, queryID, stageID string) string

ClusterTaskSubject returns the NATS subject for a task targeted at a specific cluster. Format: wadjet.tasks.<clusterID>.<type>.<queryID>.<stageID>

func ClusterTasksFilter

func ClusterTasksFilter(clusterID string) string

ClusterTasksFilter returns the filter subject for a worker to receive only its cluster's tasks.

func CompleteSubject

func CompleteSubject(queryID string) string

CompleteSubject returns the NATS subject signalling that a query has finished and per-query worker state may be released.

func CompleteSubjectAll

func CompleteSubjectAll() string

CompleteSubjectAll returns the wildcard subject for all completion messages.

func Connect

func Connect(url string, tlsCfg *tls.Config) (*nats.Conn, error)

Connect creates a NATS client connection over TCP. If tlsCfg is non-nil, the connection uses TLS (mTLS when client certs are configured).

func ConnectInProcess

func ConnectInProcess(server *natsserver.Server) (*nats.Conn, error)

ConnectInProcess creates a NATS client connection using in-process communication. This avoids TCP overhead when the client is co-located with the embedded NATS server.

func ContextWithTrace

func ContextWithTrace(ctx context.Context, tc TraceContext) context.Context

ContextWithTrace stores a TraceContext in a Go context.

func CutBaseTablePeerKey

func CutBaseTablePeerKey(peerKey string) (bucket, key string, ok bool)

CutBaseTablePeerKey decodes a peer-fetch key produced by BaseTablePeerKey. ok is false for non-base-table keys.

func DLQSubject

func DLQSubject(queryID, taskID string) string

DLQSubject returns the NATS subject for a DLQ entry.

func DirDiskUsage

func DirDiskUsage(dir string) int64

DirDiskUsage returns the total size of files in a directory (non-recursive). Returns 0 on error.

func EagerManifestSubject

func EagerManifestSubject(rootQueryID, stageID string) string

EagerManifestSubject returns the NATS subject on which the coordinator publishes ProducerTaskManifest messages for one producer stage of one root query. Consumer tasks subscribe with the exact subject (no wildcard); root and stage IDs are sanitized to NATS token characters by construction (UUIDs / stage-N names).

func EncodeDynamicFilterArtifact

func EncodeDynamicFilterArtifact(w io.Writer, a *DynamicFilterArtifact) error

EncodeDynamicFilterArtifact writes the artifact to w in WDF1 format. FilterID is NOT part of the payload — it's carried out-of-band in the upload key + partial ref, so multiple filters share the same wire codec.

func Marshal

func Marshal(v any) ([]byte, error)

Marshal serializes a message. Uses gob for ResultNotification (avoids base64 overhead on InlineData), JSON for everything else.

func NewJetStream

func NewJetStream(nc *nats.Conn) (jetstream.JetStream, error)

NewJetStream creates a JetStream context from a connection.

func NumGoroutines

func NumGoroutines() int

NumGoroutines returns the current number of goroutines.

func PriTaskClass

func PriTaskClass(deep bool) string

PriTaskClass returns the lane class token for a task: "deep" for guarded re-emitters (emitters that also consume), "leaf" otherwise.

func PriTaskSubject

func PriTaskSubject(class, taskType, queryID, stageID string) string

PriTaskSubject is TaskSubject's counterpart on the priority lane. class is PriTaskClass(task.PriorityDeep).

func ProcessRSS

func ProcessRSS() int64

ProcessRSS returns the current process resident set size in bytes.

On Linux it reads /proc/self/statm (field 2 = resident pages) × pagesize — cheaper than scanning /proc/self/status for VmRSS. This is TRUE resident memory: the Go heap AND mmap'd file pages resident in core (parquet/shuffle page cache) that runtime.MemStats.HeapInuse cannot see.

On non-Linux platforms or any read/parse error it falls back to the Go heap-in-use figure so dev/CI on macOS/Windows and tests get a sane non-zero number rather than 0 (the previous behavior). HeapInuse is a strict under-estimate (it misses mmap) — the safe direction; it never over-reports.

func QueryResultSubject

func QueryResultSubject(queryID string) string

QueryResultSubject returns the wildcard subject for all results of a query.

func ResultSubject

func ResultSubject(queryID, stageID, taskID string) string

ResultSubject returns the NATS subject for a result notification.

func ScratchQueryID

func ScratchQueryID(key string) string

ScratchQueryID extracts the root query ID from a query-scratch object key or prefix ("queries/<id>/..."), or "" when the key isn't query scratch (e.g. a table file). The root ID is the stable identity shared by every stage of one query — task QueryIDs are stage-scoped ("st-<stage>-<id>") and differ between the producer and consumer of a stage boundary, so cross-stage state (stage-output caches, streaming-exchange tokens and location hints) must key on the root, and the key is where to get it.

func SetupStreams

func SetupStreams(ctx context.Context, js jetstream.JetStream) error

SetupStreams creates the required JetStream streams for Wadjet.

func TaskProgressSubject

func TaskProgressSubject(queryID, taskID string) string

TaskProgressSubject returns the NATS subject for a specific task's per-task progress messages. Published by the worker; subscribed via wildcard by the coordinator.

func TaskRootQueryID

func TaskRootQueryID(t *Task) string

TaskRootQueryID derives a task's root query ID from its scratch prefixes and input file lists (all of which carry "queries/<id>/..." paths for stage-DAG tasks). Returns "" for tasks with no query-scratch anchors (e.g. legacy full-SQL pipeline tasks) — callers skip root-scoped bookkeeping for those.

func TaskSubject

func TaskSubject(taskType string, queryID string, stageID string) string

TaskSubject returns the NATS subject for a task of the given type.

func Unmarshal

func Unmarshal(data []byte, v any) error

Unmarshal deserializes a message. Auto-detects gob vs JSON format.

Types

type AggSpec

type AggSpec struct {
	Func      string `json:"func"` // sum, count, min, max, avg
	InputCol  string `json:"input_col"`
	OutputCol string `json:"output_col"`
	// InputExpr is the SQL text of a derived input expression, e.g.
	// "l_extendedprice * (1 - l_discount)". Empty for bare-column
	// aggregates. Native-DAG workers compile this into a Project
	// before the aggregate so HashAggregate sees a column named
	// InputCol.
	InputExpr string `json:"input_expr,omitempty"`
}

AggSpec defines an aggregation in a task.

type ComputedColSpec

type ComputedColSpec struct {
	Name string `json:"name"`
	Expr string `json:"expr"`
}

ComputedColSpec is one appended expression column on a shuffle payload (exchange subsumption dedup).

type DLQEntry

type DLQEntry struct {
	EntryID   string    `json:"entry_id"`
	TaskID    string    `json:"task_id"`
	QueryID   string    `json:"query_id"`
	StageID   string    `json:"stage_id"`
	WorkerID  string    `json:"worker_id"`
	TaskType  TaskType  `json:"task_type"`
	Error     string    `json:"error"`
	Reason    string    `json:"reason"`              // "execution_error", "panic", "marshal_error", "publish_error"
	TaskData  []byte    `json:"task_data,omitempty"` // original task JSON for replay
	Timestamp time.Time `json:"timestamp"`
}

DLQEntry records a failed task for inspection and potential retry.

type DynamicFilterArtifact

type DynamicFilterArtifact struct {
	FilterID  string
	KeyType   string
	HasRange  bool
	Min, Max  int64
	RowCount  int64
	Bloom     []uint64
	BloomMask uint64
}

DynamicFilterArtifact is the on-wire form of one filter's stats. Used for both per-task partials (sized to BloomBits, may be sparse) and the coordinator-unioned final (same size, populated bits ORed across all partials).

func DecodeDynamicFilterArtifact

func DecodeDynamicFilterArtifact(r io.Reader) (*DynamicFilterArtifact, error)

DecodeDynamicFilterArtifact reads a WDF1-format artifact from r. The caller supplies the FilterID separately (see comment on Encode).

type DynamicFilterConsume

type DynamicFilterConsume struct {
	FilterID      string `json:"filter_id"`
	SourceStageID string `json:"source_stage_id"`
	TargetColumn  string `json:"target_column"`
	KeyType       string `json:"key_type"`
}

DynamicFilterConsume, attached to a probe-scan Stage, instructs the coordinator to inject the matching BuildStats artifact into this stage's scan tasks. The stat-dep edge (SourceStageID added to Dependencies) guarantees the artifact is available before this stage dispatches.

type DynamicFilterEmit

type DynamicFilterEmit struct {
	FilterID  string `json:"filter_id"`
	KeyColumn string `json:"key_column"`
	KeyType   string `json:"key_type"` // "int32" | "int64" | "date" — integer types only in v1
	BloomBits int    `json:"bloom_bits"`
	// AtOutput places the accumulator over the task's OUTPUT stream (just
	// before the sink) instead of the scan source. Used by the semi/anti
	// build-filter pass, whose bloom must reflect the stage's post-filter /
	// post-join output keys (docs/design/semi-anti-build-dynamic-filters.md).
	AtOutput bool `json:"at_output,omitempty"`
	// StagePartials is the total number of partials the stage will produce
	// for this filter (== the stage's task count), stamped by the
	// coordinator at dispatch. When set, the emitting worker names its
	// partial key with an ".of<N>" suffix so an attach-on-arrival consumer
	// that discovers partials directly (DynamicFilterSpec.PartialPrefix)
	// learns the completeness target from any single partial — the
	// consumer usually dispatches before the emitter stage exists, so it
	// cannot be told the count in its own spec.
	StagePartials int `json:"stage_partials,omitempty"`
	// PartialPrefix is the exact S3 prefix the worker must upload this
	// filter's partials under, stamped by the coordinator alongside
	// StagePartials. The coordinator stamps the SAME value into consumer
	// DynamicFilterSpec.PartialPrefix, making it the single source of
	// truth for the partial key layout — the worker never reconstructs it.
	// Empty (legacy coordinator) ⇒ the worker falls back to the historical
	// queries/<task.QueryID>/dynfilter/<stageID>/ construction.
	PartialPrefix string `json:"partial_prefix,omitempty"`
	// GuardConsumes (guarded re-emit): FilterIDs of the emitting stage's
	// own attach-mode consumes whose blooms must retro-filter this emit's
	// buffered head rows at finalize. See the planner-side field of the
	// same name (physical.DynamicFilterEmit).
	GuardConsumes []string `json:"guard_consumes,omitempty"`
}

DynamicFilterEmit, attached to a build-scan Stage, instructs each scan task to compute a partial bloom + min/max range on KeyColumn (post-filter, post-projection). All tasks use the same BloomBits so the coordinator unions partials with a trivial bitwise OR.

type DynamicFilterPartialRef

type DynamicFilterPartialRef struct {
	FilterID string `json:"filter_id"`
	Bucket   string `json:"bucket"`
	Key      string `json:"key"`
}

DynamicFilterPartialRef is a per-task sideband artifact reference returned in ResultNotification. Each build-scan task uploads its partial stats to the indicated S3 location; the coordinator fetches all partials for a FilterID and unions them.

type DynamicFilterSpec

type DynamicFilterSpec struct {
	FilterID     string `json:"filter_id"`
	TargetColumn string `json:"target_column"`
	KeyType      string `json:"key_type"`

	HasRange bool  `json:"has_range,omitempty"`
	Min      int64 `json:"min,omitempty"`
	Max      int64 `json:"max,omitempty"`

	// Inline bloom (small filters): non-empty Bloom + BloomMask.
	Bloom     []uint64 `json:"bloom,omitempty"`
	BloomMask uint64   `json:"bloom_mask,omitempty"`

	// Staged bloom (large filters): non-empty BloomKey. BloomBits gives
	// the expected uint64 word count for size validation post-fetch.
	BloomBucket string `json:"bloom_bucket,omitempty"`
	BloomKey    string `json:"bloom_key,omitempty"`
	BloomWords  int    `json:"bloom_words,omitempty"`

	// Deferred marks an attach-on-arrival consume: the artifact at
	// BloomBucket/BloomKey may not exist yet when the task starts. The
	// worker begins scanning unfiltered, polls the key, and installs the
	// bloom mid-scan when it lands (drop-only semantics keep results
	// identical). A key that never appears (emitter withheld the filter)
	// degrades to an unfiltered scan, same as a missing filter today.
	Deferred bool `json:"deferred,omitempty"`

	// PartialPrefix (Deferred only) is the S3 prefix where the emitter
	// stage's per-task partials land. When set, the consumer's poll loop
	// additionally lists the prefix and ORs partials as they are uploaded,
	// activating the union the moment the last partial lands — instead of
	// waiting out emitter stage completion + coordinator merge + merged-key
	// staging. Activation still requires FULL coverage (every one of the
	// ".of<N>" partials merged): an incomplete union falsely rejects rows,
	// so partial-coverage filtering is never applied. Empty = merged-key
	// polling only (kill switch WADJET_DF_INCREMENTAL_PARTIALS=0).
	PartialPrefix string `json:"partial_prefix,omitempty"`
}

DynamicFilterSpec is the materialized stat the coordinator injects into a probe-scan task's OpSpec. Bloom is carried inline when the unioned bitset is small (≤ inlineThresholdBytes); otherwise BloomBucket+BloomKey point to the S3-staged artifact and the worker fetches it before scan init.

type EagerInput

type EagerInput struct {
	RootQueryID     string                 `json:"root_query_id"`
	StageID         string                 `json:"stage_id"` // producer stage
	ProducerTaskIDs []string               `json:"producer_task_ids"`
	PartitionStart  int                    `json:"partition_start"` // inclusive
	PartitionEnd    int                    `json:"partition_end"`   // inclusive
	Replay          []ProducerTaskManifest `json:"replay,omitempty"`
}

EagerInput describes one eagerly-fed input alias of a consumer task (docs/design/eager-consumer-dispatch.md §3.2): the full candidate set is nameable at dispatch (deterministic shuffle keys); existence and location stream in as ProducerTaskManifests. Replay carries manifests already published before this task was built, so the subscribe-then- replay contract never loses a completion.

type EmbeddedNATS

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

EmbeddedNATS manages an embedded NATS server with JetStream.

func NewEmbeddedNATS

func NewEmbeddedNATS(cfg NATSConfig, logger *slog.Logger) (*EmbeddedNATS, error)

NewEmbeddedNATS creates and starts an embedded NATS server.

func (*EmbeddedNATS) ClientURL

func (e *EmbeddedNATS) ClientURL() string

ClientURL returns the URL for connecting to this embedded server.

func (*EmbeddedNATS) Server

func (e *EmbeddedNATS) Server() *natsserver.Server

Server returns the underlying NATS server for in-process connections.

func (*EmbeddedNATS) Shutdown

func (e *EmbeddedNATS) Shutdown()

Shutdown stops the embedded NATS server.

type FusedJoinSpec

type FusedJoinSpec struct {
	JoinType        string            `json:"join_type"`
	JoinLeftKeys    []string          `json:"join_left_keys"`  // keys from the probe stream
	JoinRightKeys   []string          `json:"join_right_keys"` // keys in build files
	BuildFiles      []string          `json:"build_files"`     // build-side files (broadcast)
	BuildTableAlias string            `json:"build_table_alias,omitempty"`
	BuildColOrigins map[string]string `json:"build_col_origins,omitempty"` // bare build col → owning scan alias (multi-table builds only)
	JoinFilter      string            `json:"join_filter,omitempty"`
	FilterExprs     []string          `json:"filter_exprs,omitempty"` // post-join filters for this step
}

FusedJoinSpec describes an additional broadcast join absorbed into a task. The worker builds the hash table from BuildFiles, then probes each batch through this join before passing it to the next fused join (or output).

type GatherBatchMsg

type GatherBatchMsg struct {
	Terminal bool   `json:"terminal"`
	RowCount int32  `json:"row_count"`
	Payload  []byte `json:"payload,omitempty"` // WSHF-encoded single-chunk batch
	Err      string `json:"err,omitempty"`     // non-empty on terminal failure
	// WorkerID lets coord count gather batches as worker-liveness signals
	// (multi-signal liveness — see WorkerRegistry.MarkWorkerSeen). Optional;
	// older workers leave it empty.
	WorkerID string `json:"worker_id,omitempty"`
}

GatherBatchMsg is the NATS message body the worker publishes to the coordinator's gather reply subject. One message per output RecordBatch, terminated by one message with Terminal=true (zero RowCount, any Err set).

Payload is a self-contained WSHF byte stream carrying a single chunk (magic + chunk-count=1 + schema header + one row chunk). The coordinator decodes each message independently via the worker's shuffleChunkReader.

type NATSConfig

type NATSConfig struct {
	Host        string
	Port        int
	StoreDir    string   // JetStream storage directory
	MaxPayload  int32    // max message payload in bytes (default 8 MB)
	ClusterID   string   // unique cluster identifier (e.g., "central", "afb-east")
	LeafRemotes []string // remote NATS URLs for leaf node connections (edge → central)
	TLSCert     string   // TLS certificate file (server cert for coordinator)
	TLSKey      string   // TLS private key file
	TLSCA       string   // CA certificate for verifying client certs (enables mTLS)
}

NATSConfig configures the embedded NATS server.

func DefaultNATSConfig

func DefaultNATSConfig() NATSConfig

DefaultNATSConfig returns a default NATS configuration. StoreDir defaults to ~/.wadjet/nats for persistence across reboots.

type NATSUDFStore

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

NATSUDFStore manages UDF storage and propagation via NATS KV.

func NewNATSUDFStore

func NewNATSUDFStore(ctx context.Context, js jetstream.JetStream, store *expr.UDFStore, logger *slog.Logger) (*NATSUDFStore, error)

NewNATSUDFStore creates a UDF store backed by NATS KV. It creates the KV bucket if it doesn't exist.

func (*NATSUDFStore) Create

func (n *NATSUDFStore) Create(ctx context.Context, def expr.UDFDef, isAdmin bool) error

Create stores a UDF in NATS KV, which will propagate to all watchers.

func (*NATSUDFStore) Delete

func (n *NATSUDFStore) Delete(ctx context.Context, name, caller string, isAdmin bool) error

Delete removes a UDF from NATS KV.

func (*NATSUDFStore) List

func (n *NATSUDFStore) List() []expr.UDFDef

List returns all UDFs from the local store.

func (*NATSUDFStore) LoadAll

func (n *NATSUDFStore) LoadAll(ctx context.Context) error

LoadAll loads all UDFs from NATS KV into the local store. Called on startup to hydrate the store.

func (*NATSUDFStore) Watch

Watch starts watching for UDF changes in NATS KV. When another node creates or deletes a UDF, this watcher applies the change locally. The returned cancel function stops the watcher.

type OpSpec

type OpSpec struct {
	Type OpType `json:"type"`

	// Source operators (OpScan, OpShuffleSource).
	InputAlias     string   `json:"input_alias,omitempty"`  // logical alias for source-column lookup
	InputFiles     []string `json:"input_files,omitempty"`  // S3 keys to read
	InputBucket    string   `json:"input_bucket,omitempty"` // bucket override; falls back to task.DataBucket
	Columns        []string `json:"columns,omitempty"`      // projection hint (parquet column pruning)
	ScanShardIndex int      `json:"scan_shard_index,omitempty"`
	ScanShardCount int      `json:"scan_shard_count,omitempty"`

	// OpFilter.
	Predicates []string `json:"predicates,omitempty"`

	// OpProject.
	Projections []ProjectSpec `json:"projections,omitempty"`

	// OpHashJoinProbe / OpBroadcastProbe.
	JoinType        string   `json:"join_type,omitempty"`  // inner, left, semi, anti, …
	LeftKeys        []string `json:"left_keys,omitempty"`  // probe-side keys
	RightKeys       []string `json:"right_keys,omitempty"` // build-side keys
	BuildAlias      string   `json:"build_alias,omitempty"`
	BuildFiles      []string `json:"build_files,omitempty"`  // build-side input files
	BuildBucket     string   `json:"build_bucket,omitempty"` // bucket override for build files
	JoinFilter      string   `json:"join_filter,omitempty"`
	BuildRowHint    int64    `json:"build_row_hint,omitempty"`
	SemiAntiKeyOnly bool     `json:"semi_anti_key_only,omitempty"`
	// BuildFilterExprs filter the BUILD input rows before hash-table
	// insertion (exchange subsumption dedup: the dropped exchange's scan
	// filter — or its computed flag column — applied at build read).
	BuildFilterExprs    []string          `json:"build_filter_exprs,omitempty"`
	QualifyAllBuildCols bool              `json:"qualify_all_build_cols,omitempty"`
	BuildColOrigins     map[string]string `json:"build_col_origins,omitempty"` // bare build col → owning scan alias (multi-table builds only)
	OutputColumns       []string          `json:"output_columns,omitempty"`    // OutputFilter for primary probe
	LateMaterialize     bool              `json:"late_materialize,omitempty"`  // emit view-column join output (deferred gather)

	// OpExchangeSender (sink).
	ShuffleKeys   []string `json:"shuffle_keys,omitempty"`
	NumPartitions int      `json:"num_partitions,omitempty"`

	// OpGatherSink (sink).
	ReplySubject string `json:"reply_subject,omitempty"`

	// OpHashAggregate (pipeline-breaker).
	GroupByCols  []string  `json:"group_by_cols,omitempty"` // empty = scalar aggregate
	Aggregates   []AggSpec `json:"aggregates,omitempty"`    // per-column aggregations
	GroupByAll   bool      `json:"group_by_all,omitempty"`  // DISTINCT: group by every input column, key set resolved at runtime
	MergeMode    bool      `json:"merge_mode,omitempty"`    // input is already partial-aggregated; rewrite InputCol → OutputCol and COUNT → SUM
	FoldAvg      bool      `json:"fold_avg,omitempty"`      // collapse __avg_sum#X / __avg_count#X synthetics into AVG output (final aggregate only)
	BuildProject bool      `json:"build_project,omitempty"` // construct a derived-input projection before the aggregate (skipped in merge mode — partial output already has OutputCol)

	// OpSort (pipeline-breaker).
	SortKeySpecs []SortKeySpec `json:"sort_key_specs,omitempty"` // ordered key columns
	SortLimit    int           `json:"sort_limit,omitempty"`     // 0 = no limit; > 0 = top-N truncation after sort

	// OpScan (build-side, dynamic-filter producer). Each Emit makes the scan
	// task compute a partial bloom+range over the named column and upload it
	// as a sideband artifact returned in ResultNotification.DynamicFilterPartials.
	DynamicFilterEmits []DynamicFilterEmit `json:"dynamic_filter_emits,omitempty"`

	// OpScan (probe-side, dynamic-filter consumer). Coordinator-materialized
	// stats from the upstream build-scan stage. Worker wires each into the
	// row-group pruning path before the first S3 fetch.
	DynamicFilters []DynamicFilterSpec `json:"dynamic_filters,omitempty"`
}

OpSpec describes one operator within a fragment pipeline. Fields are optional — populated only for operators of the matching Type. The flat shape avoids the JSON-marshal overhead of a discriminated union; the worker's executeFragment branches on Type to read the relevant subset.

type OpType

type OpType string

OpType identifies an operator within a fragment pipeline.

const (
	// Sources (must be first in Operators).
	OpScan          OpType = "scan"           // read parquet/wshf via cachedFileStreamSource
	OpShuffleSource OpType = "shuffle_source" // read partition=NNNN/*.wshf for one partition

	// Unary transforms (middle of the pipeline; zero or more).
	OpFilter         OpType = "filter"          // FilterExprs predicate chain
	OpColumnPrune    OpType = "column_prune"    // drop columns not in OutputColumns (exec.ColumnPrune, zero-copy)
	OpHashJoinProbe  OpType = "hash_join_probe" // shuffle-side hash join: build from BuildFiles, probe upstream
	OpBroadcastProbe OpType = "broadcast_probe" // broadcast hash join: small build replicated to every task
	OpSortMergeJoin  OpType = "sort_merge_join" // big-vs-big inner join: both sides sort to runs, two-cursor merge (pipeline-breaker)

	// Pipeline-breaker operators. Consume all input from the upstream chain,
	// then emit results into the downstream chain. Splits the fragment into
	// a consume phase (source → preOps → breaker) and a drain phase
	// (breaker → postOps → sink). At most one breaker per fragment today;
	// chained breakers (e.g. aggregate + sort) need a follow-up extension.
	OpHashAggregate OpType = "hash_aggregate" // group-by + aggregates; partial or merge mode
	OpSort          OpType = "sort"           // ordered sort, optional top-N limit

	// Sinks (must be last in Operators).
	OpExchangeSender    OpType = "exchange_sender"    // partitionedShuffleSink: hash-partition into N output files
	OpUnpartitionedSink OpType = "unpartitioned_sink" // unpartitionedStageSink: single .wshf output
	OpGatherSink        OpType = "gather_sink"        // gatherReplySink: stream batches to ReplySubject

	OpProject OpType = "project" // compute SELECT-list expressions (exec.Project); output = exactly Projections
)

type OperatorPeak

type OperatorPeak struct {
	Name    string `json:"name"`    // operator instance name
	Peak    int64  `json:"peak"`    // high-water mark of OwnedBytes
	Current int64  `json:"current"` // SpillableBytes at snapshot time (reclaimable now)
	// Phase-2 AccountedOperator fields. Additive (omitempty) — gob skips them
	// for old encoders, JSON omits when zero — so this stays wire-compatible
	// with mixed-version peers. The coordinator reads none of OperatorPeaks.
	Owned    int64  `json:"owned,omitempty"`    // OwnedBytes incl. operator overhead
	Retained int64  `json:"retained,omitempty"` // RetainedBytes (detained batches)
	State    string `json:"state,omitempty"`    // OpState string
	Closed   int    `json:"closed,omitempty"`   // closed same-Name instances coalesced into this entry (max-peak shown)
}

OperatorPeak is one entry in TaskStats.OperatorPeaks. Mirrors memory.SpillableSnapshot but lives here to avoid a memory→distributed package dependency. Populated for each Spillable registered with the task's SpillManager at the moment collectTaskStats fires.

type PreComputedAggregate

type PreComputedAggregate struct {
	InputTable  string    `json:"input_table"`
	GroupByCols []string  `json:"group_by_cols"`
	AggSpecs    []AggSpec `json:"agg_specs"`
	CacheFiles  []string  `json:"cache_files"`
}

PreComputedAggregate identifies a derived aggregate whose result has already been computed and cached, paired with the S3 paths of the cache files. The worker's plan-rewrite pass matches a logical Aggregate node against this signature and replaces it with a scan of CacheFiles.

Signature semantics: Phase 1 matches only aggregates that are GROUP BY GroupByCols over a single scan of InputTable (no filters, no nested joins). AggSpecs are checked by OutputCol name so the downstream column references (e.g. __scalar_0) resolve against the cached rows unchanged.

type ProducerTaskManifest

type ProducerTaskManifest struct {
	StageID  string   `json:"stage_id"`
	TaskID   string   `json:"task_id"`
	Attempt  int      `json:"attempt"` // attempt fencing (memo §5)
	Files    []string `json:"files"`   // keys that EXIST (empty partitions absent)
	WorkerID string   `json:"worker_id"`
	// PeerAddr is the producing worker's peer-exchange address, resolved
	// by the coordinator at publish time. Empty when the worker is not
	// serving peer fetches — consumers fall through to S3 as always.
	PeerAddr string `json:"peer_addr,omitempty"`
	// Final marks the manifest of the producer stage's last terminal
	// task; a consumer that has resolved every candidate and seen Final
	// may EOF its manifest feed.
	Final bool `json:"final,omitempty"`
}

ProducerTaskManifest announces one completed producer task's shuffle output files to eagerly-dispatched consumers (docs/design/ eager-consumer-dispatch.md §3.1). Published by the coordinator on EagerManifestSubject(root, stage) as each producer task reaches a successful terminal state; metadata only.

type ProjectSpec

type ProjectSpec struct {
	Expr string `json:"expr"`
	Name string `json:"name"`
	Type int    `json:"type,omitempty"`
}

ProjectSpec is one output column of an OpProject: Name is the emitted column, Expr the SQL expression the worker compiles (bare column references become passthrough copies). Type is the plan-time inferred parquet.TypeID for computed expressions (0 = resolve from the source column) — the worker can't infer it from the input schema because the output column doesn't exist there.

type QueryManifest

type QueryManifest struct {
	QueryID     string   `json:"query_id"`
	ResultFiles []string `json:"result_files"`
	TotalRows   int64    `json:"total_rows"`
	TotalBytes  int64    `json:"total_bytes"`
}

QueryManifest describes the final results of a query.

type ResultNotification

type ResultNotification struct {
	TaskID   string `json:"task_id"`
	QueryID  string `json:"query_id"`
	StageID  string `json:"stage_id"`
	WorkerID string `json:"worker_id"`
	Success  bool   `json:"success"`
	Error    string `json:"error,omitempty"`

	// Result location
	ResultPath  string   `json:"result_path,omitempty"`
	ResultFiles []string `json:"result_files,omitempty"` // multi-file output (e.g., shuffle per-partition files)
	// UploadPendingKeys lists the ResultFiles whose durable (S3) copy was
	// still uploading in the background when this notification was sent
	// (streaming exchange Phase B). The coordinator's reap grace counts
	// these per worker: a silent worker holding the only copy of such
	// keys gets a bounded reap deferral (docs/design/reap-grace.md).
	// Keys leave pending via UploadComplete. Absent (older worker or
	// synchronous upload) = nothing pending — grace disengages.
	UploadPendingKeys []string `json:"upload_pending_keys,omitempty"`
	NumRows           int64    `json:"num_rows"`
	SizeBytes         int64    `json:"size_bytes"`

	// Per-partition output accounting for partition-writing tasks (shuffle
	// tasks and fragment tasks with an exchange-sender sink), indexed by
	// partition id with len == NumPartitions. Rows come from the partitioned
	// sink's per-partition counters; bytes are the on-disk uncompressed
	// .wshf sizes (same unit as SizeBytes). The coordinator reduces these
	// element-wise across a stage's tasks to detect hot partitions at the
	// repartition→join seam. Empty partitions hold zeros; nil = worker
	// didn't report (legacy build or non-partitioned output).
	PartitionRows  []int64 `json:"partition_rows,omitempty"`
	PartitionBytes []int64 `json:"partition_bytes,omitempty"`

	// Small result fast path (< 256 KB): inline result data
	InlineData []byte `json:"inline_data,omitempty"`

	// Distributed tracing context (from originating task)
	TraceID string `json:"trace_id,omitempty"`
	SpanID  string `json:"span_id,omitempty"` // worker's span for this task

	Duration  time.Duration `json:"duration"`
	Timestamp time.Time     `json:"timestamp"`

	// Task execution stats (populated by worker for debugging)
	TaskStats *TaskStats `json:"task_stats,omitempty"`

	// DynamicFilterPartials, populated when the originating task was a
	// build-scan with DynamicFilterEmit set. One ref per emit. Coordinator
	// fetches+unions before dispatching the downstream probe-scan stage.
	DynamicFilterPartials []DynamicFilterPartialRef `json:"dynamic_filter_partials,omitempty"`

	// MissingInputKey (streaming exchange Phase B), set on failure when the
	// task could not resolve an input file that carried a peer-location
	// hint: peer fetch failed AND the durable copy was absent past the
	// bounded re-poll. The coordinator classifies it against the producing
	// worker's liveness and the key's durability bit — producer dead with
	// the key not durable is ErrInputLost (unrecoverable by task retry).
	MissingInputKey string `json:"missing_input_key,omitempty"`
}

ResultNotification is sent by workers when a task completes.

type SortKeySpec

type SortKeySpec struct {
	Column string `json:"column"`
	Desc   bool   `json:"desc"`
}

SortKeySpec defines a sort key in a task.

type Task

type Task struct {
	ID        string   `json:"id"`
	QueryID   string   `json:"query_id"`
	StageID   string   `json:"stage_id"`
	Type      TaskType `json:"type"`
	ClusterID string   `json:"cluster_id,omitempty"` // target cluster for routing
	TableName string   `json:"table_name,omitempty"`
	// Attempt is the 1-based execution attempt for this task ID. Stage
	// inputs are durable S3 files and outputs are overwrite-safe (same
	// TaskID → same key), so the coordinator re-dispatches failed tasks
	// with the same ID and a bumped Attempt (coordinator.taskRetrier).
	// 0 means unset (pre-retry senders); treat as attempt 1.
	Attempt int `json:"attempt,omitempty"`
	// EstimatedBytes is the coordinator's estimate of this task's input
	// footprint, used for memory-aware admission (worker holds the task
	// start until the shared pool has room for it) and coordinator-side
	// bin-packing. 0 = unknown — admission and placement fall back to
	// pressure-threshold / round-robin behavior. Doubled on every
	// re-dispatch (grow-on-retry, the Trino FTE pattern): a task whose
	// worker died possibly-of-memory is only admitted where strictly more
	// headroom exists.
	EstimatedBytes int64 `json:"estimated_bytes,omitempty"`
	// Priority routes the task onto the latency-critical lane
	// (SubjectPriTasksAll → dedicated worker slots outside MaxConcurrent).
	// Set only for dimension-class tasks whose completion unblocks bulk
	// work (dyn-filter emitter scans); their smallness is enforced by the
	// planner passes that mark them.
	Priority bool `json:"priority,omitempty"`
	// PriorityDeep sub-classes the priority lane: emitter tasks that ALSO
	// consume dynamic filters (guarded re-emit mid-scans) ride a slot pool
	// SEPARATE from leaf emitters. A guarded task blocks at finalize until
	// its consumed bloom settles; if it could occupy the slots its own
	// upstream leaf emitter needs, the lane deadlocks until the poll
	// deadline (observed SF100 2026-08-07, trt ead0976: Q07 stalled ~10min,
	// hop-B held both lane slots waiting on the dim task queued behind it).
	// Class-disjoint pools make the circular wait structurally impossible,
	// cross-query included.
	PriorityDeep bool `json:"priority_deep,omitempty"`

	// Pipeline-specific (full query on one worker)
	SQLText    string `json:"sql_text,omitempty"`    // SQL query to execute as standalone pipeline
	DataBucket string `json:"data_bucket,omitempty"` // bucket containing source data (tables)

	// Scan-split pipeline: table scans distributed across workers, compute on one worker.
	// Maps scan alias → result file paths from pre-scanned data.
	// Alias is unique per scan node: "table" or "table:N" for self-joins.
	PreScannedInputs map[string][]string `json:"pre_scanned_inputs,omitempty"`

	// Probe-split pipeline: the probe table's files are partitioned across workers
	// while build tables are scanned in full by each worker. Maps scan alias →
	// allowed file paths. Only the probe scan alias has a restricted file list;
	// other scans read all files normally.
	ScanFileFilter map[string][]string `json:"scan_file_filter,omitempty"`

	// Row-group sharding for single-file scans. When ScanShardCount > 1, the
	// worker reads only row groups [idx*N/count, (idx+1)*N/count) of each
	// input parquet file (where N = file's NumRowGroups). The dispatcher
	// uses this to fan out a single compacted file (e.g. SF10 partsupp =
	// one 691 MB file) into multiple parallel scan tasks; without it the
	// downstream broadcast-join chain cascades single-tasked because
	// `broadcastJoinProbeSplit` requires probe upstream to have ≥ 2 files.
	// ScanShardCount = 0 or 1 means no sharding (whole file).
	ScanShardIndex int `json:"scan_shard_index,omitempty"`
	ScanShardCount int `json:"scan_shard_count,omitempty"`

	// PartialAggregate is set on probe-split pipeline tasks to indicate that
	// the top-level Sort and Limit should be stripped. Each worker produces
	// complete partial aggregates; the coordinator merges them.
	PartialAggregate bool `json:"partial_aggregate,omitempty"`

	// Scan-specific
	Files           []string          `json:"files,omitempty"`
	PartitionFilter map[string]string `json:"partition_filter,omitempty"`
	// Columns has dual semantics by stage type:
	//   - scan / shuffle tasks: input projection (columns to read from
	//     parquet/wshf source).
	//   - hash_join / broadcast_join tasks: output projection — the worker
	//     applies these as the probe operator's OutputFilter so the join
	//     emits only what the downstream stage consumes, instead of the
	//     full union of build+probe schemas.
	//   - aggregate / sort tasks: ignored (output schema is determined by
	//     AggSpecs / SortKeys).
	Columns     []string `json:"columns,omitempty"`
	FilterExprs []string `json:"filter_exprs,omitempty"` // SQL filter expressions for pushdown
	// PostFilterExprs are SQL filter expressions applied to the stage's
	// OUTPUT (post-aggregate/post-join) rather than to raw scan input.
	// Native-DAG compute stages use this for HAVING and join residual
	// predicates. FilterExprs vs PostFilterExprs differ in column scope:
	// FilterExprs references scan columns; PostFilterExprs references
	// aggregate output cols or joined-schema cols.
	PostFilterExprs []string `json:"post_filter_exprs,omitempty"`

	// Fused scan-aggregate: partial aggregation done at scan level
	ScanAggGroupBy []string  `json:"scan_agg_group_by,omitempty"`
	ScanAggSpecs   []AggSpec `json:"scan_agg_specs,omitempty"`

	// Aggregate-specific
	GroupByCols []string  `json:"group_by_cols,omitempty"`
	Aggregates  []AggSpec `json:"aggregates,omitempty"`
	InputFiles  []string  `json:"input_files,omitempty"` // results from previous stage

	// Sort-specific
	SortKeys       []SortKeySpec `json:"sort_keys,omitempty"`
	Limit          int           `json:"limit,omitempty"`
	MergePreSorted bool          `json:"merge_pre_sorted,omitempty"` // true for merge_sort: inputs are pre-sorted
	MergePartials  bool          `json:"merge_partials,omitempty"`   // true for final_aggregate: re-aggregate partial results

	// Join-specific
	JoinType        string   `json:"join_type,omitempty"`         // inner, left, right, full, cross
	JoinLeftKeys    []string `json:"join_left_keys,omitempty"`    // probe side key columns
	JoinRightKeys   []string `json:"join_right_keys,omitempty"`   // build side key columns
	BuildFiles      []string `json:"build_files,omitempty"`       // build (right) side input files
	BuildTableAlias string   `json:"build_table_alias,omitempty"` // build-side alias for column disambiguation
	// QualifyAllBuildCols, when true, forces the join executor to emit
	// build-side columns under their qualified name even when no probe-side
	// column has the same base name. Set by the planner for self-join scenarios
	// (Q07's two scans of nation that co-path into the same join chain).
	QualifyAllBuildCols bool `json:"qualify_all_build_cols,omitempty"`
	// BuildColOrigins maps bare build-column names (lowercased) to their
	// owning scan alias. Set only for multi-table build subtrees (bushy
	// shapes); the executor qualifies duplicate build columns with the
	// owning alias instead of BuildTableAlias.
	BuildColOrigins map[string]string `json:"build_col_origins,omitempty"`
	JoinFilter      string            `json:"join_filter,omitempty"` // semi/anti join inequality filter expression
	// BuildFilterExprs filter the build input rows before hash-table
	// insertion (exchange subsumption dedup).
	BuildFilterExprs []string `json:"build_filter_exprs,omitempty"`

	// Fused join: additional broadcast joins absorbed into a single task.
	// The worker builds hash tables for each fused join, then chains probes
	// batch-by-batch: probe → join1 → join2 → ... → output.
	FusedJoins []FusedJoinSpec `json:"fused_joins,omitempty"`

	// Window-specific
	WindowCols []WindowColSpec `json:"window_cols,omitempty"`

	// Shuffle-specific
	ShuffleKeys   []string `json:"shuffle_keys,omitempty"`   // columns to hash-partition on
	NumPartitions int      `json:"num_partitions,omitempty"` // number of output partitions
	// ComputedCols are expression columns appended to the shuffle payload
	// after the projected scan columns (exchange subsumption dedup: a
	// dropped filtered sibling's filter ships as a computed flag).
	ComputedCols []ComputedColSpec `json:"computed_cols,omitempty"`
	// DropCols are read-only helper columns (ComputedCols expression
	// inputs) removed from the payload after the flags are computed.
	DropCols []string `json:"drop_cols,omitempty"`
	// PartialAggKeys/PartialAggSpecs enable sender-side partial
	// aggregation inside the shuffle task (exchange partial agg): rows
	// are pre-combined on PartialAggKeys with name-preserving
	// SUM/MIN/MAX specs (OutputCol == InputCol) before partitioning.
	// Only set when the planner proved every consumer of the exchange
	// merge-compatible; the reduction is exchange-internal and invisible
	// downstream.
	PartialAggKeys  []string  `json:"partial_agg_keys,omitempty"`
	PartialAggSpecs []AggSpec `json:"partial_agg_specs,omitempty"`
	PartitionID     int       `json:"partition_id,omitempty"` // which partition this join task handles

	// Dynamic filters carried at the top level for non-fragment task shapes
	// (TaskTypeShuffle). Fragment tasks carry the same data in OpSpec.DynamicFilters
	// per-op, but shuffle tasks use a flat task descriptor and apply the
	// filter against their single implicit scan source. The worker materializes
	// these into the cachedFileStreamSource's bloom+range pushdown.
	DynamicFilters []DynamicFilterSpec `json:"dynamic_filters,omitempty"`

	// Distributed tracing context (W3C Trace Context format)
	TraceID    string `json:"trace_id,omitempty"`    // 32-char hex
	SpanID     string `json:"span_id,omitempty"`     // 16-char hex parent span
	TraceFlags byte   `json:"trace_flags,omitempty"` // 0x01 = sampled

	// Identity context (for access control enforcement at workers)
	IdentityName string `json:"identity_name,omitempty"`
	IdentityRole string `json:"identity_role,omitempty"`

	// ABAC pre-evaluated policy decisions (serialized for worker enforcement)
	PolicyDecisionJSON json.RawMessage `json:"policy_decision,omitempty"`

	// Result destination
	ResultBucket string `json:"result_bucket"`
	ResultPrefix string `json:"result_prefix"`

	// PreComputedAggregates carries pre-computed derived-aggregate results
	// that the worker should substitute for in-plan aggregate subtrees.
	// Matches on (input_table, group_by_cols, aggregate specs); when a
	// logical-plan aggregate node matches a signature here, the worker
	// replaces it with a scan of the provided cache files. Populated by
	// the coordinator when PickAggregateShuffleCandidate + preCompute
	// succeed (spec: 2026-04-18-shuffle-distributed-aggregate.md).
	PreComputedAggregates []PreComputedAggregate `json:"pre_computed_aggregates,omitempty"`

	// Inputs maps scan/alias name → S3 keys for upstream stage output.
	// Generalizes PreScannedInputs: used for both table-scan inputs (legacy)
	// and previous-stage-output inputs (Phase 3 native DAG). Worker source
	// selection inspects file patterns: partition=NNNN/*.wshf → partitionShardSource;
	// *.parquet → streamSource.
	Inputs map[string][]string `json:"inputs,omitempty"`

	// InputLocations maps an input S3 key to the peer-exchange address of
	// the worker that produced the file and still holds it on local disk
	// (streaming exchange Phase A, docs/design/streaming-exchange.md).
	// Best-effort hints: a consumer tries one peer fetch per hinted key and
	// falls through to KV/S3 on any failure — the S3 keys stay canonical,
	// so a task spec re-sent verbatim on retry works with or without the
	// hints. Only populated when the coordinator runs --streaming-exchange.
	InputLocations map[string]string `json:"input_locations,omitempty"`

	// AffinityWorkerID is the rendezvous-hash owner of this scan task's
	// base-table files (docs/design/scan-affinity.md): the worker whose
	// NVMe base-table cache canonically holds them. Placement PREFERENCE
	// only — the scheduler falls through to binpack/round-robin when the
	// worker is absent or the same-batch cap bites, and a task placed
	// elsewhere just misses the cache exactly as before.
	AffinityWorkerID string `json:"affinity_worker_id,omitempty"`

	// EagerInputs maps an input alias to its eager manifest-feed
	// descriptor (docs/design/eager-consumer-dispatch.md). When an alias
	// appears here, the worker builds a manifest-fed source for it
	// instead of consuming a frozen file list; other aliases of the same
	// task keep their explicit Inputs entries.
	EagerInputs map[string]EagerInput `json:"eager_inputs,omitempty"`

	// FetchToken authorizes peer-exchange fetches for this task's query.
	// Producers record it (to validate incoming FetchShuffle requests
	// against); consumers present it. Minted per QueryID by the
	// coordinator; empty when streaming exchange is disabled.
	FetchToken string `json:"fetch_token,omitempty"`

	// AsyncUpload (streaming exchange Phase B) tells the worker to report
	// task completion once stage-output files are finalized on local disk
	// (and adopted into the LocalStageCache for peer serving), continuing
	// the S3 upload in the background. The worker publishes UploadComplete
	// when the task's uploads land; until then the S3 copy may not exist
	// and consumers rely on the peer tier (with a bounded S3 re-poll).
	// Only set by the coordinator for native-DAG task types whose outputs
	// are consumed by workers; false = today's synchronous upload.
	AsyncUpload bool `json:"async_upload,omitempty"`

	// UploadPolicy (docs/design/shuffle-durability.md) refines AsyncUpload:
	// how urgently the durable S3 copy of this task's stage outputs must
	// exist. Empty = eager (background upload starts immediately —
	// pre-knob behavior, and what workers that predate the field do).
	// "lazy" = the worker queues the upload jobs unstarted and runs them
	// only on a demand signal (SubjectUploadRelease broadcast or worker
	// drain); jobs still queued when the query completes are elided.
	// "off" = never upload; producer death before consumption degrades to
	// the coordinator's one-shot streaming-disabled re-execution.
	// Only meaningful when AsyncUpload is true; the coordinator keeps
	// stages whose outputs it reads itself (scalar-subquery producers) on
	// eager, because the coordinator has no peer tier.
	UploadPolicy UploadPolicy `json:"upload_policy,omitempty"`

	// Output is the S3 prefix where this task's output is materialized.
	// Shuffle/pipeline-intermediate: worker writes "<Output>partition=NNNN/<taskID>.wshf".
	// Pipeline-final (before Gather): single-partition output at "<Output><taskID>.wshf".
	// Gather: empty; worker streams to ReplySubject.
	Output string `json:"output,omitempty"`

	// ReplySubject is the NATS subject the worker publishes batch chunks to.
	// Only set for TaskTypeGather; enables real-operator Gather semantics.
	ReplySubject string `json:"reply_subject,omitempty"`

	// GatherOrdering (Gather only) — merge-sort keys applied by the coordinator
	// when reassembling output from multiple gather workers. Empty means no
	// ordering; coordinator concatenates streams in arrival order.
	GatherOrdering []SortKeySpec `json:"gather_ordering,omitempty"`

	// GatherLimit (Gather only) — top-N limit applied by the coordinator
	// after ordering. Zero means no limit.
	GatherLimit int `json:"gather_limit,omitempty"`

	// StageType discriminates TaskTypeStage variants: "scan", "hash_join",
	// "broadcast_join", "aggregate", "sort", "merge_sort", "window",
	// "final_aggregate". Matches physical.Stage.Type strings. Empty for
	// non-TaskTypeStage tasks.
	StageType string `json:"stage_type,omitempty"`

	// BuildRowHint is the planner's estimate of build-side row count,
	// used to pre-size the hash table arena. Populated for TaskTypeStage
	// hash_join stages. Zero means no hint (arena grows dynamically).
	BuildRowHint int64 `json:"build_row_hint,omitempty"`

	// SemiAntiKeyOnly is set on semi/anti hash_join stages without a
	// SemiAntiFilter — enables key-only build (skip batch storage).
	SemiAntiKeyOnly bool `json:"semi_anti_key_only,omitempty"`

	// Operators carries a multi-operator pipeline the worker runs end-to-end
	// without inter-operator round-trips through S3+NATS. When set, the worker
	// builds an exec.Pipeline from these specs in order: Operators[0] is the
	// source, Operators[len-1] is the sink, and the operators in between are
	// unary transforms. Single-operator stages can be expressed as a single
	// OpSpec; multi-operator fragments (the long-term shape that dissolves
	// the per-operator S3 round-trip floor) carry the full pipeline.
	//
	// Worker dispatch: Execute → executeStage → if len(Operators) > 0
	// → executeFragment; otherwise fall back to the per-StageType handlers
	// (executeStageScan/HashJoin/Aggregate/Sort). The legacy handlers stay
	// alive until every shape has migrated; mixed routing during migration
	// is intentional and safe.
	Operators []OpSpec `json:"operators,omitempty"`

	CreatedAt time.Time `json:"created_at"`
}

Task is the unit of distributed work published to NATS JetStream.

type TaskProgress

type TaskProgress struct {
	QueryID        string    `json:"query_id"`
	StageID        string    `json:"stage_id"`
	TaskID         string    `json:"task_id"`
	WorkerID       string    `json:"worker_id"`
	RowsProcessed  int64     `json:"rows_processed"`
	BytesProcessed int64     `json:"bytes_processed,omitempty"`
	Timestamp      time.Time `json:"timestamp"`
}

TaskProgress is published by a worker from inside a task's hot loop to signal forward progress (rows/bytes processed). Coord uses these to distinguish a slow-but-healthy task from a wedged task.

Workers emit at most one TaskProgress message per ~2s per task; the counters are monotonically increasing across the task's lifetime, so the coord can compute throughput and detect "no row progress for N seconds" stalls without needing every batch to publish.

type TaskStats

type TaskStats struct {
	MemUsed       int64          `json:"mem_used"`                 // memory tracker usage at completion
	MemBudget     int64          `json:"mem_budget"`               // memory budget for this task
	SpillFiles    int            `json:"spill_files"`              // number of spill files written
	SpillBytes    int64          `json:"spill_bytes"`              // total bytes spilled to disk
	RSS           int64          `json:"rss"`                      // worker process RSS at task completion
	PeakHeapMB    int64          `json:"peak_heap_mb"`             // per-task peak HeapAlloc in MB, captured by atomic-max sampler
	TrackerPeak   int64          `json:"tracker_peak,omitempty"`   // peak of the per-task memory.Tracker (Reserve-tracked bytes)
	OperatorPeaks []OperatorPeak `json:"operator_peaks,omitempty"` // per-Spillable peak attribution at task end
	// Phase-4 accounting observability (additive, omitempty; the coordinator
	// reads neither — diagnostic only).
	DriftMB   int64 `json:"drift_mb,omitempty"`    // HeapInuse − (operator owned + reservoir actual) at task end
	MmapRSSMB int64 `json:"mmap_rss_mb,omitempty"` // max(0, RSS − HeapInuse): non-heap resident working set
}

TaskStats captures per-task execution metrics for debugging.

type TaskType

type TaskType string

TaskType identifies the kind of work a task performs.

const (
	TaskTypePipeline TaskType = "pipeline" // full query executed as standalone pipeline on one worker
	TaskTypeShuffle  TaskType = "shuffle"  // hash-partitions input rows into N output partition files
	TaskTypeGather   TaskType = "gather"   // streams pipeline output to ReplySubject via gatherReplySink
	TaskTypeStage    TaskType = "stage"    // single-operator stage fragment (native-DAG Phase 3)
)

type TraceContext

type TraceContext struct {
	TraceID    string // 32-char hex
	SpanID     string // 16-char hex
	TraceFlags byte   // 0x01 = sampled
}

TraceContext carries distributed trace identifiers across service boundaries.

func NewTraceContext

func NewTraceContext() TraceContext

NewTraceContext generates a new root trace with a random trace ID and span ID.

func TraceFromContext

func TraceFromContext(ctx context.Context) TraceContext

TraceFromContext extracts a TraceContext from a Go context. Returns zero value if none is set.

func (TraceContext) NewSpanID

func (tc TraceContext) NewSpanID() TraceContext

NewSpanID generates a new random span ID within the same trace.

type UDFEntry

type UDFEntry struct {
	Name   string   `json:"name"`
	Params []string `json:"params"`
	Body   string   `json:"body"`
	Owner  string   `json:"owner,omitempty"`
	Locked bool     `json:"locked,omitempty"`
}

UDFEntry is the JSON-serializable form of a UDF stored in NATS KV.

type UploadComplete

type UploadComplete struct {
	RootQueryID string   `json:"root_query_id"`
	TaskID      string   `json:"task_id"`
	WorkerID    string   `json:"worker_id"`
	Keys        []string `json:"keys"`
	// Failed marks uploads abandoned after retries (S3 outage) or
	// cancelled (query terminal). Keys stay non-durable; ErrInputLost
	// remains the backstop if the producer also dies.
	Failed bool `json:"failed,omitempty"`
}

UploadComplete (streaming exchange Phase B) is published by a worker when an async-upload task's background S3 uploads have all landed. The coordinator flips the per-key durability bits it uses to classify missing-input failures and to gate its own direct reads of stage output (scalar-subquery extraction).

type UploadPolicy

type UploadPolicy string

UploadPolicy is the shuffle-durability mode a task's stage-output uploads run under (docs/design/shuffle-durability.md). Carried per task so the policy survives mixed-version clusters: workers that predate the field unmarshal it away and upload eagerly, which is always safe.

const (
	UploadEager UploadPolicy = ""     // background upload starts immediately (default)
	UploadLazy  UploadPolicy = "lazy" // queue unstarted; run on release/drain; elide at query end
	UploadOff   UploadPolicy = "off"  // never upload scratch; rely on peers + whole-query re-execution
)

type WindowColSpec

type WindowColSpec struct {
	Func        string        `json:"func"`      // row_number, rank, dense_rank, sum, count, avg, min, max
	InputCol    string        `json:"input_col"` // for aggregate window functions
	OutputCol   string        `json:"output_col"`
	PartitionBy []string      `json:"partition_by,omitempty"`
	OrderBy     []SortKeySpec `json:"order_by,omitempty"`
}

WindowColSpec defines a window function column in a task.

type WorkerHeartbeat

type WorkerHeartbeat struct {
	WorkerID      string   `json:"worker_id"`
	ClusterID     string   `json:"cluster_id,omitempty"`     // cluster this worker belongs to
	MaxConcurrent int      `json:"max_concurrent,omitempty"` // worker's effective task slot count (after auto-tuning); 0 = unknown
	ActiveTasks   int      `json:"active_tasks"`
	ActiveTaskIDs []string `json:"active_task_ids,omitempty"` // task IDs currently executing
	MemoryUsed    int64    `json:"memory_used"`
	MemoryTotal   int64    `json:"memory_total"`
	PoolUsed      int64    `json:"pool_used,omitempty"`   // bytes Reserved in the worker's shared memory pool
	PoolBudget    int64    `json:"pool_budget,omitempty"` // shared memory pool capacity in bytes; pressure = PoolUsed/PoolBudget
	RSS           int64    `json:"rss,omitempty"`         // process RSS from /proc/self/status
	NumGoroutines int      `json:"num_goroutines,omitempty"`
	Mallocs       uint64   `json:"mallocs,omitempty"`         // cumulative allocation count from runtime.MemStats
	SpillDiskUsed int64    `json:"spill_disk_used,omitempty"` // bytes used in spill directory
	Draining      bool     `json:"draining,omitempty"`        // true when worker is draining
	// PeerAddr is the worker's dialable peer-exchange (FetchShuffle)
	// address. Empty when the worker doesn't serve peer fetches — the
	// coordinator then simply emits no location hints referencing it,
	// which makes mixed-version/mixed-flag rollouts self-gating.
	PeerAddr  string    `json:"peer_addr,omitempty"`
	Timestamp time.Time `json:"timestamp"`
}

WorkerHeartbeat is periodically sent by workers.

Jump to

Keyboard shortcuts

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