import "github.com/altessa-s/go-atlas/service/dispatch"Package dispatch provides a generic, non-blocking, batching dispatch
engine with optional crash-safe persistence backed by
core/io/wal.
It is the foundation for fire-and-forget producers (audit logs, request access logs, telemetry) where the hot path must not block on I/O but events must still survive process crashes.
Without a WAL, Engine.Submit only sends the item to an in-memory channel
and returns. With WithWAL enabled, Submit additionally serializes the
item via the configured Codec, appends it to the active WAL segment
(page-cache write, no fsync) and then sends it to the channel. A
background goroutine fsyncs the active segment every fsync interval
(group commit), bounding the loss window after a crash.
Worker goroutines drain the channel, build batches up to BatchSize or
FlushInterval, and call Sink.StoreBatch. On success the corresponding
WAL records are acked; once all records in a sealed segment are acked,
the segment file is removed. Sinks receive at-least-once delivery: a
single item may reappear after a crash-and-replay sequence, so
implementations should be idempotent on the records' natural identity
(e.g. event ID).
| Scenario | Guarantee |
|---|---|
Graceful Shutdown(ctx) |
Full drain to Sink, acked segments removed |
SIGKILL / panic / OOM |
Records past the last fsync are replayed on next Start |
| Loss window | Bounded by the WAL fsync interval (default 5 ms) |
| Sink temporarily unavailable | Records accumulate in the WAL up to its configured max bytes |
| WAL disabled | Zero I/O on the submit path; loses buffered items on crash |
| Function / Method | Description |
|---|---|
NewEngine[T] |
Construct an Engine for item type T with a Sink[T] |
Engine.Start |
Open the WAL, replay unflushed records, start workers |
Engine.Submit |
Non-blocking enqueue (returns false on full-buffer drop) |
Engine.Shutdown |
Drain pending items and close the WAL, bounded by ctx |
Engine.Dropped |
Total items dropped at submit time |
Engine.WAL |
Underlying *wal.WAL, or nil when WAL is disabled |
| Option | Effect |
|---|---|
WithBufferSize |
In-memory queue capacity (default 10000) |
WithBatchSize |
Max batch size passed to Sink.StoreBatch (default 100) |
WithFlushInterval |
Partial-batch flush deadline (default 1 s) |
WithWorkers |
Number of dispatch goroutines (default 2) |
WithRetryAttempts |
Max retries per failed batch (default 3) |
WithRetryBackoff |
Exponential backoff base (default 100 ms) |
WithBackPressure |
Block Submit when the buffer is full instead of dropping |
WithOnDrop |
Callback invoked on submit-time drop |
WithLogger |
*slog.Logger for diagnostic messages |
WithCollector |
metrics.Collector (metrics emitted under the subsystem) |
WithMetricsSubsystem |
Override the metrics subsystem name (default "async") |
WithWAL |
Enable crash-safe WAL; requires a Codec[T] |
The default metrics subsystem is
"async"(not"dispatch") so dashboards and alerts built against the earlier location of this package keep working after the rename. Override it viaWithMetricsSubsystemwhen embedding the engine inside a facade.
The write-ahead log lives in its own package,
core/io/wal, and can be used independently of this
engine. WithWAL(dir, codec, walOpts...) wires it in; the full API
surface, durability guarantees, and performance notes are documented
in the core/io/wal README. Only the
wal.Option variadic tail is exposed through this package — everything
else is the WAL package's surface.
sink := &mySink{} // implements dispatch.Sink[*Event]
codec := myCodec{} // implements dispatch.Codec[*Event]
eng, err := dispatch.NewEngine[*Event](sink,
dispatch.WithBatchSize[*Event](100),
dispatch.WithFlushInterval[*Event](time.Second),
dispatch.WithWAL[*Event]("/var/lib/svc/wal", codec,
wal.WithMaxSegmentBytes(64<<20),
wal.WithFsyncInterval(5*time.Millisecond),
),
)
if err != nil {
return err
}
if err := eng.Start(); err != nil {
return err
}
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_ = eng.Shutdown(ctx)
}()
eng.Submit(&Event{...}) // non-blocking hot pathFor a runnable end-to-end example, see Example in example_test.go.
Use dispatch.Engine when you need a non-blocking producer interface
with batching and optional local durability. Prefer a dedicated facade
for domain-specific producers (audit, request logs) over a direct
dependency on this package; build a new facade only when the schema and
sink semantics genuinely differ.
Do not use it as a distributed queue: delivery is to a single
in-process Sink, and the WAL is a local, per-process spool. Cross-node
durability belongs to the broker layer.