spate_datagen/lib.rs
1//! Synthetic commerce-event source for Spate, giving a pipeline you can run
2//! with nothing installed.
3//!
4//! Every other source in this workspace needs infrastructure before it says
5//! anything: a broker, a bucket, a coordination store. That prerequisite is
6//! the first thing between a reader and a running pipeline, and it is the only
7//! thing this crate removes. Point a pipeline at a [`DatagenSource`] and it
8//! produces a stream of storefront events (orders, their payments, and
9//! refunds against those payments) on as many partitions as you ask for, at a
10//! rate you set, for as long as you want.
11//!
12//! ```yaml
13//! source:
14//! datagen:
15//! partitions: 4
16//! events_per_tick: 10
17//! tick_interval: 100ms # 400 events/s in total
18//! count: 10000 # omit for an unbounded stream
19//! ```
20//!
21//! # A dataset, not a schema language
22//!
23//! `datagen` generates **one built-in, named dataset**. There is no `fields:`
24//! map, and adding one is out of scope rather than unimplemented.
25//!
26//! The reason is the thing that makes the stream worth generating. A payment
27//! must name an order that was really placed, for an amount that matches its
28//! lines, on the same partition, at a later offset. That is a property
29//! of the whole dataset, not of any field in it. A field-wise schema can say
30//! "a `u64` here"; it cannot say "this `u64`, drawn from the ids the same lane
31//! minted earlier and not yet drawn". A named dataset gets the property for
32//! free and stays forty lines of configuration.
33//!
34//! Datasets are enumerated by [`Dataset`]; the storefront model lives in
35//! [`storefront`].
36//!
37//! # Referential integrity without coordination
38//!
39//! Each lane owns a **disjoint** slice of the order-id space
40//! (`order_id = n × partitions + lane_index`) and keeps its own bounded ring
41//! of the orders it has placed and captured. A payment or a refund is drawn
42//! from that ring, so it always references an order:
43//!
44//! - the same lane minted, and therefore the **same partition**;
45//! - at a **strictly greater offset**, because the lane released the order
46//! first.
47//!
48//! A payment settles the order's line total exactly. A refund is that total,
49//! or its half, third or quarter rounded down, never more than was captured,
50//! which is what a balance check downstream rests on.
51//!
52//! The mix places faster than it captures, so orders placed and never paid
53//! accumulate for as long as the pipeline runs; `open_orders` reports how
54//! many. A check reconciles the orders that were captured, not all of them.
55//!
56//! No lane reads another lane's state, so nothing is shared on the record
57//! path and the whole property survives the CPU-pinned fan-out. The payload
58//! key carries the order id, so
59//! [`KeyHashRouter`](spate_core::sink::KeyHashRouter) colocates an order and
60//! its payment in one sink shard.
61//!
62//! # Delivery, stated plainly
63//!
64//! **This is a demo and test source. Do not build a production pipeline on
65//! it.**
66//!
67//! - `commit()` stores watermarks **in memory and nowhere else**. They are
68//! readable through [`DatagenSource::committed`], published as
69//! `committed_offset` when `metrics.per_partition_detail` is on, and gone
70//! when the process exits.
71//! - The source claims **no resumability**. A restart begins every lane at
72//! offset 0, so with a fixed seed the entire stream is regenerated from the
73//! beginning, which is strictly *more* duplication than a real
74//! at-least-once source, which would replay only from its last committed
75//! position.
76//! - A `resume_from:` file is **declined**. A demo source that appears to
77//! resume durably gets built on, and the failure surfaces as silent data
78//! loss in a deployment that never meant to take one.
79//!
80//! Opening the source logs a `WARN` saying so, once, on the same principle.
81//!
82//! # Metrics
83//!
84//! Families under `spate_datagen_source_*`: `events_generated_total{event}`,
85//! `ticks_total` and `tick_overrun_total` are counted by the lanes;
86//! `events_remaining`, `open_orders` and (with `metrics.per_partition_detail`)
87//! `committed_offset{partition}` are published by the control plane.
88//!
89//! There is deliberately no `spate_source_lag_records`. For an unbounded
90//! generator the lag is infinite, so the series would exist or not depending
91//! on whether `count` was set.
92
93mod config;
94mod dims;
95mod encode;
96mod events;
97mod lane;
98mod metrics;
99mod plan;
100mod rng;
101mod source;
102
103pub use config::{Clock, DatagenSourceConfig, Dataset, Encoding};
104pub use dims::{CUSTOMERS, REGIONS, SKUS};
105pub use events::{
106 EVENT_SCHEMA_JSON, OrderLine, OrderPlaced, PaymentCaptured, RefundIssued, StorefrontEvent,
107};
108pub use lane::{DatagenBatch, DatagenLane};
109pub use source::DatagenSource;
110
111/// The storefront dataset's event model, under the name a pipeline assembly
112/// reads best:
113///
114/// ```
115/// use spate_datagen::storefront::{OrderLine, OrderPlaced, PaymentCaptured, RefundIssued};
116/// ```
117///
118/// The same types are re-exported at the crate root; this module is the
119/// spelling to prefer when a file also imports the source and its
120/// configuration, because it says which dataset the events belong to.
121pub mod storefront {
122 pub use crate::events::{
123 OrderLine, OrderPlaced, PaymentCaptured, RefundIssued, StorefrontEvent,
124 };
125}