1use std::{
20 fmt, io,
21 marker::PhantomData,
22 num::NonZeroU32,
23 pin::Pin,
24 result,
25 sync::{Arc, LazyLock, PoisonError},
26 time::{Duration, SystemTime},
27};
28
29use governor::{InsufficientCapacity, Jitter, Quota, RateLimiter};
30use nisshi_client::{Client, ConnectionManager};
31use nisshi_otel::meter_provider;
32use nisshi_sans_io::{
33 ByteSize, ErrorCode, ProduceRequest,
34 produce_request::{PartitionProduceData, TopicProduceData},
35 record::{deflated, inflated},
36};
37use nisshi_schema::{Generator as _, Registry, Schema};
38use nonzero_ext::nonzero;
39use opentelemetry::{
40 InstrumentationScope, KeyValue, global,
41 metrics::{Counter, Histogram, Meter},
42};
43use opentelemetry_otlp::ExporterBuildError;
44use opentelemetry_sdk::error::OTelSdkError;
45use opentelemetry_semantic_conventions::SCHEMA_URL;
46use tokio::{
47 signal::unix::{SignalKind, signal},
48 task::JoinSet,
49 time::sleep,
50};
51use tokio_util::sync::CancellationToken;
52use tracing::{Instrument, Level, debug, span};
53use url::Url;
54
55pub(crate) static METER: LazyLock<Meter> = LazyLock::new(|| {
56 global::meter_with_scope(
57 InstrumentationScope::builder(env!("CARGO_PKG_NAME"))
58 .with_version(env!("CARGO_PKG_VERSION"))
59 .with_schema_url(SCHEMA_URL)
60 .build(),
61 )
62});
63
64pub type Result<T, E = Error> = result::Result<T, E>;
65
66#[derive(thiserror::Error, Debug)]
67pub enum Error {
68 Api(ErrorCode),
69 Client(#[from] nisshi_client::Error),
70 ExporterBuild(#[from] ExporterBuildError),
71 InsufficientCapacity(#[from] InsufficientCapacity),
72 Io(Arc<io::Error>),
73 Otel(#[from] nisshi_otel::Error),
74 OtelSdk(#[from] OTelSdkError),
75 Poison,
76 Protocol(#[from] nisshi_sans_io::Error),
77 Schema(Box<nisshi_schema::Error>),
78 SchemaNotFoundForTopic(String),
79 UnknownHost(String),
80 Url(#[from] url::ParseError),
81}
82
83impl<T> From<PoisonError<T>> for Error {
84 fn from(_value: PoisonError<T>) -> Self {
85 Self::Poison
86 }
87}
88
89impl From<nisshi_schema::Error> for Error {
90 fn from(error: nisshi_schema::Error) -> Self {
91 Self::Schema(Box::new(error))
92 }
93}
94
95impl From<io::Error> for Error {
96 fn from(value: io::Error) -> Self {
97 Self::Io(Arc::new(value))
98 }
99}
100
101impl fmt::Display for Error {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 write!(f, "{self:?}")
104 }
105}
106
107#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
108pub enum CancelKind {
109 Interrupt,
110 Terminate,
111 Timeout,
112}
113
114#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
115pub struct Configuration {
116 broker: Url,
117 topic: String,
118 partition: i32,
119 schema_registry: Url,
120 batch_size: u32,
121 per_second: Option<u32>,
122 throughput: Option<u32>,
123 producers: u32,
124 duration: Option<Duration>,
125 otlp_endpoint_url: Option<Url>,
126}
127
128#[derive(Clone, Debug)]
129pub struct Generate {
130 configuration: Configuration,
131 registry: Registry,
132}
133
134impl TryFrom<Configuration> for Generate {
135 type Error = Error;
136
137 fn try_from(configuration: Configuration) -> Result<Self, Self::Error> {
138 Registry::builder_try_from_url(&configuration.schema_registry)
139 .map(|builder| builder.build())
140 .map(|registry| Self {
141 configuration,
142 registry,
143 })
144 .map_err(Into::into)
145 }
146}
147
148static GENERATE_PRODUCE_BATCH_DURATION: LazyLock<Histogram<u64>> = LazyLock::new(|| {
149 METER
150 .u64_histogram("generate_produce_batch_duration")
151 .with_unit("ms")
152 .with_description("Generate a produce batch in milliseconds")
153 .build()
154});
155
156static PRODUCE_REQUEST_RESPONSE_DURATION: LazyLock<Histogram<u64>> = LazyLock::new(|| {
157 METER
158 .u64_histogram("produce_request_response_duration")
159 .with_unit("ms")
160 .with_description("Latency of receiving an produce response in milliseconds")
161 .build()
162});
163
164fn frame(name: String, index: i32, schema: Schema, batch_size: i32) -> Result<deflated::Frame> {
165 let attributes = [
166 KeyValue::new("topic", name.clone()),
167 KeyValue::new("partition", index.to_string()),
168 KeyValue::new("batch_size", batch_size.to_string()),
169 ];
170
171 let start = SystemTime::now();
172
173 let mut batch = inflated::Batch::builder();
174 let offset_deltas = 0..batch_size;
175
176 for offset_delta in offset_deltas {
177 batch = schema
178 .generate()
179 .map(|record| record.offset_delta(offset_delta))
180 .map(|record| batch.record(record))?;
181 }
182
183 batch
184 .last_offset_delta(batch_size)
185 .build()
186 .map(|batch| inflated::Frame {
187 batches: vec![batch],
188 })
189 .and_then(deflated::Frame::try_from)
190 .inspect(|_| {
191 GENERATE_PRODUCE_BATCH_DURATION.record(
192 start
193 .elapsed()
194 .map_or(0, |duration| duration.as_millis() as u64),
195 &attributes,
196 )
197 })
198 .map_err(Into::into)
199}
200
201pub async fn produce(
202 client: Client,
203 name: String,
204 index: i32,
205 batch_size: i32,
206 frame: deflated::Frame,
207) -> Result<()> {
208 debug!(?client, %name, index, batch_size);
209
210 let attributes = [
211 KeyValue::new("topic", name.clone()),
212 KeyValue::new("partition", index.to_string()),
213 KeyValue::new("batch_size", batch_size.to_string()),
214 ];
215
216 let req = ProduceRequest::default().topic_data(Some(
217 [TopicProduceData::default().name(name).partition_data(Some(
218 [PartitionProduceData::default()
219 .index(index)
220 .records(Some(frame))]
221 .into(),
222 ))]
223 .into(),
224 ));
225
226 let start = SystemTime::now();
227
228 let response = client.call(req).await.inspect(|_| {
229 PRODUCE_REQUEST_RESPONSE_DURATION.record(
230 start
231 .elapsed()
232 .map_or(0, |duration| duration.as_millis() as u64),
233 &attributes,
234 )
235 })?;
236
237 assert!(
238 response
239 .responses
240 .unwrap_or_default()
241 .into_iter()
242 .all(|topic| {
243 topic
244 .partition_responses
245 .unwrap_or_default()
246 .iter()
247 .inspect(|partition| debug!(topic = %topic.name, ?partition))
248 .all(|partition| partition.error_code == i16::from(ErrorCode::None))
249 })
250 );
251
252 Ok(())
253}
254
255static RATE_LIMIT_DURATION: LazyLock<Histogram<u64>> = LazyLock::new(|| {
256 METER
257 .u64_histogram("rate_limit_duration")
258 .with_unit("ms")
259 .with_description("Rate limit latencies in milliseconds")
260 .build()
261});
262
263static PRODUCE_RECORD_COUNT: LazyLock<Counter<u64>> = LazyLock::new(|| {
264 METER
265 .u64_counter("produce_record_count")
266 .with_description("Produced record count")
267 .build()
268});
269
270static PRODUCE_API_DURATION: LazyLock<Histogram<u64>> = LazyLock::new(|| {
271 METER
272 .u64_histogram("produce_duration")
273 .with_unit("ms")
274 .with_description("Produce API latencies in milliseconds")
275 .build()
276});
277
278impl Generate {
279 pub async fn main(self) -> Result<ErrorCode> {
280 debug!(configuration = ?self.configuration);
281
282 let meter_provider = self
283 .configuration
284 .otlp_endpoint_url
285 .map_or(Ok(None), |otlp_endpoint_url| {
286 meter_provider(otlp_endpoint_url, env!("CARGO_PKG_NAME")).map(Some)
287 })?;
288
289 let Some(schema) = self.registry.schema(&self.configuration.topic).await? else {
290 return Err(Error::SchemaNotFoundForTopic(
291 self.configuration.topic.clone(),
292 ));
293 };
294
295 let mut interrupt_signal = signal(SignalKind::interrupt()).unwrap();
296 debug!(?interrupt_signal);
297
298 let mut terminate_signal = signal(SignalKind::terminate()).unwrap();
299 debug!(?terminate_signal);
300
301 let rate_limiter = self
302 .configuration
303 .per_second
304 .or(self.configuration.throughput)
305 .and_then(NonZeroU32::new)
306 .map(Quota::per_second)
307 .map(RateLimiter::direct)
308 .map(Arc::new)
309 .inspect(|rate_limiter| debug!(?rate_limiter));
310
311 let batch_size = NonZeroU32::new(self.configuration.batch_size)
312 .inspect(|batch_size| debug!(batch_size = batch_size.get()))
313 .unwrap_or(nonzero!(10u32));
314
315 let mut set = JoinSet::new();
316
317 let token = CancellationToken::new();
318
319 let client = ConnectionManager::builder(self.configuration.broker)
320 .client_id(Some(env!("CARGO_PKG_NAME").into()))
321 .build()
322 .await
323 .inspect(|pool| debug!(?pool))
324 .map(Client::new)?;
325
326 for producer in 0..self.configuration.producers {
327 let rate_limiter = rate_limiter.clone();
328 let schema = schema.clone();
329 let topic = self.configuration.topic.clone();
330 let partition = self.configuration.partition;
331 let token = token.clone();
332 let client = client.clone();
333
334 _ = set.spawn(async move {
335 let span = span!(Level::DEBUG, "producer", producer);
336
337 async move {
338 let attributes = [KeyValue::new("producer", producer.to_string())];
339
340 loop {
341 debug!(%topic, partition);
342
343 let Ok(frame) = frame(topic.clone(), partition, schema.clone(), batch_size.get() as i32) else {
344 break
345 };
346
347 if let Some(ref rate_limiter) = rate_limiter {
348 let rate_limit_start = SystemTime::now();
349
350
351 let cells = self.configuration.throughput.and(frame.size_in_bytes().ok().and_then(|bytes|NonZeroU32::new(bytes as u32))).unwrap_or(batch_size);
352 debug!(cells);
353
354
355 tokio::select! {
356 cancelled = token.cancelled() => {
357 debug!(?cancelled);
358 break
359 },
360
361 Ok(_) = rate_limiter.until_n_ready_with_jitter(cells, Jitter::up_to(Duration::from_millis(50))) => {
362 RATE_LIMIT_DURATION.record(
363 rate_limit_start
364 .elapsed()
365 .inspect(|duration|debug!(rate_limit_duration_ms = duration.as_millis()))
366 .map_or(0, |duration| duration.as_millis() as u64),
367 &attributes)
368
369 },
370 }
371 }
372
373 let produce_start = SystemTime::now();
374
375 tokio::select! {
376 cancelled = token.cancelled() => {
377 debug!(?cancelled);
378 break
379 },
380
381 Ok(_) = produce(client.clone(), topic.clone(), partition, batch_size.get() as i32, frame) => {
382 PRODUCE_RECORD_COUNT.add(batch_size.get() as u64, &attributes);
383 PRODUCE_API_DURATION.record(produce_start.elapsed().inspect(|duration|debug!(produce_duration_ms = duration.as_millis())).map_or(0, |duration| duration.as_millis() as u64), &attributes);
384 },
385 }
386 }
387
388 }.instrument(span).await;
389
390
391 });
392 }
393
394 let join_all = async {
395 while !set.is_empty() {
396 debug!(len = set.len());
397 _ = set.join_next().await;
398 }
399 };
400
401 let duration = self
402 .configuration
403 .duration
404 .map(sleep)
405 .map(Box::pin)
406 .map(|pinned| pinned as Pin<Box<dyn Future<Output = ()>>>)
407 .unwrap_or(Box::pin(std::future::pending()) as Pin<Box<dyn Future<Output = ()>>>);
408
409 let cancellation = tokio::select! {
410
411 timeout = duration => {
412 debug!(?timeout);
413 token.cancel();
414 Some(CancelKind::Timeout)
415 }
416
417 completed = join_all => {
418 debug!(?completed);
419 None
420 }
421
422 interrupt = interrupt_signal.recv() => {
423 debug!(?interrupt);
424 Some(CancelKind::Interrupt)
425 }
426
427 terminate = terminate_signal.recv() => {
428 debug!(?terminate);
429 Some(CancelKind::Terminate)
430 }
431
432 };
433
434 debug!(?cancellation);
435
436 if let Some(meter_provider) = meter_provider {
437 meter_provider
438 .force_flush()
439 .inspect(|force_flush| debug!(?force_flush))?;
440
441 meter_provider
442 .shutdown()
443 .inspect(|shutdown| debug!(?shutdown))?;
444 }
445
446 if let Some(CancelKind::Timeout) = cancellation {
447 sleep(Duration::from_secs(5)).await;
448 }
449
450 debug!(abort = set.len());
451 set.abort_all();
452
453 while !set.is_empty() {
454 _ = set.join_next().await;
455 }
456
457 Ok(ErrorCode::None)
458 }
459
460 pub fn builder()
461 -> Builder<PhantomData<Url>, PhantomData<String>, PhantomData<i32>, PhantomData<Url>> {
462 Builder::default()
463 }
464}
465
466#[derive(Clone, Debug)]
467pub struct Builder<B, T, P, S> {
468 broker: B,
469 topic: T,
470 partition: P,
471 schema_registry: S,
472 batch_size: u32,
473 per_second: Option<u32>,
474 throughput: Option<u32>,
475 producers: u32,
476 duration: Option<Duration>,
477 otlp_endpoint_url: Option<Url>,
478}
479
480impl Default
481 for Builder<PhantomData<Url>, PhantomData<String>, PhantomData<i32>, PhantomData<Url>>
482{
483 fn default() -> Self {
484 Self {
485 broker: Default::default(),
486 topic: Default::default(),
487 partition: Default::default(),
488 schema_registry: Default::default(),
489 batch_size: 1,
490 per_second: None,
491 throughput: None,
492 producers: 1,
493 duration: None,
494 otlp_endpoint_url: None,
495 }
496 }
497}
498
499impl<B, T, P, S> Builder<B, T, P, S> {
500 pub fn broker(self, broker: impl Into<Url>) -> Builder<Url, T, P, S> {
501 Builder {
502 broker: broker.into(),
503 topic: self.topic,
504 partition: self.partition,
505 schema_registry: self.schema_registry,
506 batch_size: self.batch_size,
507 per_second: self.per_second,
508 throughput: self.throughput,
509 producers: self.producers,
510 duration: self.duration,
511 otlp_endpoint_url: self.otlp_endpoint_url,
512 }
513 }
514
515 pub fn topic(self, topic: impl Into<String>) -> Builder<B, String, P, S> {
516 Builder {
517 broker: self.broker,
518 topic: topic.into(),
519 partition: self.partition,
520 schema_registry: self.schema_registry,
521 batch_size: self.batch_size,
522 per_second: self.per_second,
523 throughput: self.throughput,
524 producers: self.producers,
525 duration: self.duration,
526 otlp_endpoint_url: self.otlp_endpoint_url,
527 }
528 }
529
530 pub fn partition(self, partition: i32) -> Builder<B, T, i32, S> {
531 Builder {
532 broker: self.broker,
533 topic: self.topic,
534 partition,
535 schema_registry: self.schema_registry,
536 batch_size: self.batch_size,
537 per_second: self.per_second,
538 throughput: self.throughput,
539 producers: self.producers,
540 duration: self.duration,
541 otlp_endpoint_url: self.otlp_endpoint_url,
542 }
543 }
544
545 pub fn schema_registry(self, schema_registry: Url) -> Builder<B, T, P, Url> {
546 Builder {
547 broker: self.broker,
548 topic: self.topic,
549 partition: self.partition,
550 schema_registry,
551 batch_size: self.batch_size,
552 per_second: self.per_second,
553 throughput: self.throughput,
554 producers: self.producers,
555 duration: self.duration,
556 otlp_endpoint_url: self.otlp_endpoint_url,
557 }
558 }
559
560 pub fn batch_size(self, batch_size: u32) -> Self {
561 Self { batch_size, ..self }
562 }
563
564 pub fn per_second(self, per_second: Option<u32>) -> Self {
565 Self { per_second, ..self }
566 }
567
568 pub fn throughput(self, throughput: Option<u32>) -> Self {
569 Self { throughput, ..self }
570 }
571
572 pub fn producers(self, producers: u32) -> Self {
573 Self { producers, ..self }
574 }
575
576 pub fn duration(self, duration: Option<Duration>) -> Self {
577 Self { duration, ..self }
578 }
579
580 pub fn otlp_endpoint_url(self, otlp_endpoint_url: Option<Url>) -> Self {
581 Self {
582 otlp_endpoint_url,
583 ..self
584 }
585 }
586}
587
588impl Builder<Url, String, i32, Url> {
589 pub fn build(self) -> Result<Generate> {
590 Generate::try_from(Configuration {
591 broker: self.broker,
592 topic: self.topic,
593 partition: self.partition,
594 schema_registry: self.schema_registry,
595 batch_size: self.batch_size,
596 per_second: self.per_second,
597 throughput: self.throughput,
598 producers: self.producers,
599 duration: self.duration,
600 otlp_endpoint_url: self.otlp_endpoint_url,
601 })
602 }
603}