laminar_connectors/generator/
mod.rs1use std::sync::Arc;
12use std::time::Instant;
13
14use arrow_array::builder::BinaryBuilder;
15use arrow_array::{Int64Array, RecordBatch, StringArray, UInt32Array};
16use arrow_schema::{DataType, Field, Schema, SchemaRef};
17use async_trait::async_trait;
18
19use crate::checkpoint::SourceCheckpoint;
20use crate::config::{ConfigKeySpec, ConnectorConfig, ConnectorInfo};
21use crate::connector::{
22 SourceBatch, SourceConnector, SourceConsistency, SourceContract, SourceInputMode,
23 SourcePosition, SourceRowPositionCapability, SourceRowPositions, SourceStart, SourceTopology,
24};
25use crate::error::ConnectorError;
26use crate::registry::ConnectorRegistry;
27
28const GENERATOR_CHECKPOINT_CONNECTOR: &str = "generator";
29const CHECKPOINT_VERSION_METADATA: &str = "checkpoint.version";
30const GENERATOR_CHECKPOINT_VERSION: &str = "2";
31
32fn generator_input_channel(source_name: &str) -> Result<Vec<u8>, ConnectorError> {
33 let source_len = u32::try_from(source_name.len()).map_err(|_| {
34 ConnectorError::ConfigurationError(
35 "generator source identity exceeds the input-channel encoding limit".into(),
36 )
37 })?;
38 let mut channel = Vec::with_capacity(source_name.len() + 8);
39 channel.extend_from_slice(&source_len.to_be_bytes());
40 channel.extend_from_slice(source_name.as_bytes());
41 channel.extend_from_slice(&0_u32.to_be_bytes());
42 Ok(channel)
43}
44
45fn validate_generator_checkpoint(checkpoint: &SourceCheckpoint) -> Result<(), ConnectorError> {
46 match checkpoint.get_metadata("connector") {
47 Some(GENERATOR_CHECKPOINT_CONNECTOR) => {}
48 Some(connector) => {
49 return Err(ConnectorError::ConfigurationError(format!(
50 "generator checkpoint belongs to connector '{connector}'"
51 )));
52 }
53 None => {
54 return Err(ConnectorError::ConfigurationError(
55 "generator checkpoint is missing connector identity".into(),
56 ));
57 }
58 }
59 if checkpoint.get_metadata(CHECKPOINT_VERSION_METADATA) != Some(GENERATOR_CHECKPOINT_VERSION) {
60 return Err(ConnectorError::ConfigurationError(format!(
61 "generator checkpoint requires {CHECKPOINT_VERSION_METADATA}={GENERATOR_CHECKPOINT_VERSION}"
62 )));
63 }
64 Ok(())
65}
66
67pub struct GeneratorSource {
69 schema: SchemaRef,
70 rows_per_second: u64,
71 batch_max: usize,
72 max_rows: Option<u64>,
73 input_channel: Vec<u8>,
74 next_seq: u64,
77 anchor: Option<(Instant, u64)>,
82}
83
84impl GeneratorSource {
85 fn generator_schema() -> SchemaRef {
86 Arc::new(Schema::new(vec![
87 Field::new("seq", DataType::Int64, false),
88 Field::new("ts_ms", DataType::Int64, false),
89 Field::new("value", DataType::Utf8, false),
90 ]))
91 }
92
93 #[allow(
98 clippy::cast_possible_wrap,
99 clippy::cast_sign_loss,
100 clippy::cast_possible_truncation,
101 clippy::cast_precision_loss
102 )]
103 fn build_batch(&self, start: u64, n: usize) -> Result<RecordBatch, ConnectorError> {
104 let seqs: Vec<i64> = (0..n as u64).map(|i| (start + i) as i64).collect();
105 let ts: Vec<i64> = seqs
106 .iter()
107 .map(|&s| {
108 (s as u64)
109 .saturating_mul(1000)
110 .wrapping_div(self.rows_per_second) as i64
111 })
112 .collect();
113 let values: Vec<String> = seqs.iter().map(|s| format!("v{s}")).collect();
114 RecordBatch::try_new(
115 Arc::clone(&self.schema),
116 vec![
117 Arc::new(Int64Array::from(seqs)),
118 Arc::new(Int64Array::from(ts)),
119 Arc::new(StringArray::from(values)),
120 ],
121 )
122 .map_err(|e| ConnectorError::ReadError(e.to_string()))
123 }
124
125 fn build_row_positions(
126 &self,
127 start: u64,
128 n: usize,
129 ) -> Result<SourceRowPositions, ConnectorError> {
130 let mut partitions =
131 BinaryBuilder::with_capacity(n, self.input_channel.len().saturating_mul(n));
132 let mut order_keys = BinaryBuilder::with_capacity(n, 8_usize.saturating_mul(n));
133 for row in 0..n {
134 let sequence = start
135 .checked_add(u64::try_from(row).map_err(|_| {
136 ConnectorError::Internal("generator batch row index exceeds u64".into())
137 })?)
138 .ok_or_else(|| ConnectorError::Internal("generator sequence overflow".into()))?;
139 partitions.append_value(&self.input_channel);
140 order_keys.append_value(sequence.to_be_bytes());
141 }
142 SourceRowPositions::try_new(
143 partitions.finish(),
144 order_keys.finish(),
145 UInt32Array::from(vec![0; n]),
146 )
147 }
148}
149
150impl Default for GeneratorSource {
151 fn default() -> Self {
152 let input_channel = generator_input_channel("generator")
153 .expect("the built-in generator source identity is valid");
154 Self {
155 schema: Self::generator_schema(),
156 rows_per_second: 1000,
157 batch_max: 1024,
158 max_rows: None,
159 input_channel,
160 next_seq: 0,
161 anchor: None,
162 }
163 }
164}
165
166#[async_trait]
167impl SourceConnector for GeneratorSource {
168 fn contract(&self, _config: &ConnectorConfig) -> Result<SourceContract, ConnectorError> {
169 Ok(SourceContract::new(
170 SourceConsistency::Replayable,
171 SourceTopology::Singleton,
172 SourceInputMode::AppendOnly,
173 )
174 .with_row_positions(SourceRowPositionCapability::OrderedDeterministic)
175 .with_exact_delivery_certification())
176 }
177
178 async fn start(&mut self, request: SourceStart) -> Result<(), ConnectorError> {
179 let (config, position, _) = request.into_parts();
180 let source_name = config
181 .get("laminar.source.name")
182 .filter(|name| !name.is_empty())
183 .unwrap_or("generator");
184 self.input_channel = generator_input_channel(source_name)?;
185 if let Some(rps) = config.get_parsed::<u64>("rows.per.second")? {
186 if rps == 0 {
187 return Err(ConnectorError::ConfigurationError(
188 "rows.per.second must be > 0".into(),
189 ));
190 }
191 self.rows_per_second = rps;
192 }
193 if let Some(n) = config.get_parsed::<usize>("batch.max.size")? {
194 self.batch_max = n.max(1);
195 }
196 self.max_rows = config.get_parsed::<u64>("max.rows")?;
197
198 self.next_seq = match position {
199 SourcePosition::Initial => 0,
200 SourcePosition::Resume {
201 attempt,
202 checkpoint,
203 } => {
204 validate_generator_checkpoint(&checkpoint)?;
205 if checkpoint.input_channels() != Some(std::slice::from_ref(&self.input_channel)) {
206 return Err(ConnectorError::ConfigurationError(format!(
207 "generator checkpoint {attempt:?} has the wrong input-channel inventory"
208 )));
209 }
210 let seq = checkpoint.get_offset("seq").ok_or_else(|| {
211 ConnectorError::ConfigurationError(format!(
212 "generator checkpoint {attempt:?} is missing required 'seq' offset"
213 ))
214 })?;
215 seq.parse().map_err(|e| {
216 ConnectorError::ConfigurationError(format!(
217 "bad generator offset '{seq}' in checkpoint {attempt:?}: {e}"
218 ))
219 })?
220 }
221 };
222 self.anchor = None;
225 Ok(())
226 }
227
228 #[allow(
230 clippy::cast_possible_truncation,
231 clippy::cast_sign_loss,
232 clippy::cast_precision_loss
233 )]
234 async fn poll_batch(
235 &mut self,
236 max_records: usize,
237 ) -> Result<Option<SourceBatch>, ConnectorError> {
238 let (anchored_at, anchor_seq) = *self
239 .anchor
240 .get_or_insert_with(|| (Instant::now(), self.next_seq));
241 let mut allowed = anchor_seq.saturating_add(
242 (anchored_at.elapsed().as_secs_f64() * self.rows_per_second as f64) as u64,
243 );
244 if let Some(max) = self.max_rows {
245 allowed = allowed.min(max);
246 }
247 let pending = allowed.saturating_sub(self.next_seq);
248 let n = (pending as usize).min(max_records).min(self.batch_max);
249 if n == 0 {
250 return Ok(None);
251 }
252 let batch = self.build_batch(self.next_seq, n)?;
253 let row_positions = self.build_row_positions(self.next_seq, n)?;
254 self.next_seq =
255 self.next_seq
256 .checked_add(u64::try_from(n).map_err(|_| {
257 ConnectorError::Internal("generator batch size exceeds u64".into())
258 })?)
259 .ok_or_else(|| ConnectorError::Internal("generator sequence overflow".into()))?;
260 Ok(Some(SourceBatch::positioned(batch, row_positions)?))
261 }
262
263 fn schema(&self) -> SchemaRef {
264 Arc::clone(&self.schema)
265 }
266
267 fn checkpoint(&self) -> SourceCheckpoint {
268 let mut cp = SourceCheckpoint::new();
269 cp.set_offset("seq", self.next_seq.to_string());
270 cp.set_metadata("connector", GENERATOR_CHECKPOINT_CONNECTOR);
271 cp.set_metadata(CHECKPOINT_VERSION_METADATA, GENERATOR_CHECKPOINT_VERSION);
272 cp.set_input_channels(vec![self.input_channel.clone()])
273 .expect("the cached generator input-channel identity is valid");
274 cp
275 }
276
277 async fn close(&mut self) -> Result<(), ConnectorError> {
278 Ok(())
279 }
280}
281
282pub fn register_generator_source(registry: &ConnectorRegistry) -> Result<(), ConnectorError> {
288 let info = ConnectorInfo {
289 name: "generator".to_string(),
290 display_name: "Synthetic Data Generator".to_string(),
291 version: env!("CARGO_PKG_VERSION").to_string(),
292 is_source: true,
293 is_sink: false,
294 config_keys: vec![
295 ConfigKeySpec::optional("rows.per.second", "Emission rate", "1000"),
296 ConfigKeySpec::optional("batch.max.size", "Max rows per batch", "1024"),
297 ConfigKeySpec::optional(
298 "max.rows",
299 "Stop after this many rows (unbounded if unset)",
300 "",
301 ),
302 ],
303 };
304 registry.register_source(
305 "generator",
306 info,
307 Arc::new(|_registry: Option<&Arc<prometheus::Registry>>| {
308 Ok(Box::new(GeneratorSource::default()))
309 }),
310 )
311}
312
313#[cfg(test)]
314mod tests;