nisshi_service/lib.rs
1// Copyright ⓒ 2024-2026 Peter Morgan <peter.james.morgan@gmail.com>
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Common service layers used in other Nisshi crates.
16//!
17//! ## Overview
18//!
19//! This crate provides [Layer][`rama::Layer`] and [Service][`rama::Service`]
20//! implementations for operating on [`Frame`], [`Body`], [Request][`nisshi_sans_io::Request`]
21//! and [Response][`nisshi_sans_io::Response`].
22//!
23//! The following transports are provided:
24//!
25//! - TCP with [`TcpBytesLayer`] and [`BytesTcpService`].
26//! - [`MPSC channel`][`tokio::sync::mpsc`] with [`ChannelFrameLayer`] and [`ChannelFrameService`].
27//! - [`Bytes`][`bytes::Bytes`] with [`BytesLayer`] (designed primarily for protocol testing)
28//!
29//! ### Routing
30//!
31//! Route [`Frame`] to services using [`FrameRouteService`] to automatically
32//! implement [`ApiVersionsRequest`][`nisshi_sans_io::ApiVersionsRequest`]
33//! with valid protocol ranges:
34//!
35//! ```
36//! # use nisshi_service::Error;
37//! # #[tokio::main]
38//! # async fn main() -> Result<(), Error> {
39//! # use rama::{Context, Layer as _, Service as _};
40//! # use nisshi_sans_io::{ApiKey as _, ApiVersionsRequest, MetadataRequest, MetadataResponse};
41//! # use nisshi_service::{
42//! # BytesFrameLayer, BytesFrameService, BytesLayer, BytesService, FrameBytesLayer,
43//! # FrameBytesService, FrameRouteService, RequestFrameLayer, RequestFrameService, RequestLayer,
44//! # ResponseService,
45//! # };
46//! let frame_route = FrameRouteService::<(), Error>::builder()
47//! .with_service(
48//! RequestLayer::<MetadataRequest>::new().into_layer(ResponseService::new(|_, _| {
49//! Ok(MetadataResponse::default()
50//! .brokers(Some([].into()))
51//! .topics(Some([].into()))
52//! .cluster_id(Some("nisshi".into()))
53//! .controller_id(Some(111))
54//! .throttle_time_ms(Some(0))
55//! .cluster_authorized_operations(Some(-1)))
56//! })),
57//! )
58//! .and_then(|builder| builder.build())?;
59//! # Ok(())
60//! # }
61//! ```
62//!
63//! ### Layering
64//!
65//! Composing [`RequestFrameLayer`], [`FrameBytesLayer`], [`BytesLayer`],
66//! [`BytesFrameLayer`] together into `frame_route` to implement a test protocol stack.
67//!
68//! A "client" [`Frame`] is marshalled into bytes using [`FrameBytesLayer`], with [`BytesLayer`] connecting
69//! to a "server" that demarshalls using [`BytesFrameLayer`] back into frames,
70//! routing into `frame_route` (above) to [`MetadataRequest`][`nisshi_sans_io::MetadataRequest`]
71//! or [`ApiVersionsRequest`][`nisshi_sans_io::ApiVersionsRequest`] depending on the
72//! [API key][`Frame#method.api_key`]:
73//!
74//! ```
75//! # use nisshi_service::Error;
76//! # #[tokio::main]
77//! # async fn main() -> Result<(), Error> {
78//! # use rama::{Context, Layer as _, Service as _};
79//! # use nisshi_sans_io::{ApiKey as _, ApiVersionsRequest, MetadataRequest, MetadataResponse};
80//! # use nisshi_service::{
81//! # BytesFrameLayer, BytesFrameService, BytesLayer, BytesService, FrameBytesLayer,
82//! # FrameBytesService, FrameRouteService, RequestFrameLayer, RequestFrameService, RequestLayer,
83//! # ResponseService,
84//! # };
85//! # let frame_route = FrameRouteService::<(), Error>::builder()
86//! # .with_service(
87//! # RequestLayer::<MetadataRequest>::new().into_layer(ResponseService::new(|_, _| {
88//! # Ok(MetadataResponse::default()
89//! # .brokers(Some([].into()))
90//! # .topics(Some([].into()))
91//! # .cluster_id(Some("nisshi".into()))
92//! # .controller_id(Some(111))
93//! # .throttle_time_ms(Some(0))
94//! # .cluster_authorized_operations(Some(-1)))
95//! # })),
96//! # )
97//! # .and_then(|builder| builder.build())?;
98//! let service = (
99//! // "client" initiator side:
100//! RequestFrameLayer,
101//! FrameBytesLayer,
102//!
103//! // transport
104//! BytesLayer,
105//!
106//! // "server" side:
107//! BytesFrameLayer::default(),
108//! )
109//! .into_layer(frame_route);
110//! # Ok(())
111//! # }
112//! ```
113//!
114//! In the broker, proxy and CLI clients, [`BytesLayer`] is replaced with
115//! [`TcpBytesLayer`] (server side) or [`BytesTcpService`] (client/initiator side).
116//!
117//! ### Servicing
118//!
119//! We construct a default [`service context`][`rama::Context`] and a
120//! [`MetadataRequest`][`nisshi_sans_io::MetadataRequest`] to initiate a request
121//! on the `service`. The request passes through the protocol stack
122//! and routed into our service. The service responds with a
123//! [`MetadataResponse`][`nisshi_sans_io::MetadataResponse`], so that we can
124//! verify the expected `response.cluster_id`:
125//!
126//! ```
127//! # use nisshi_service::Error;
128//! # #[tokio::main]
129//! # async fn main() -> Result<(), Error> {
130//! # use rama::{Context, Layer as _, Service as _};
131//! # use nisshi_sans_io::{ApiKey as _, ApiVersionsRequest, MetadataRequest, MetadataResponse};
132//! # use nisshi_service::{
133//! # BytesFrameLayer, BytesFrameService, BytesLayer, BytesService, FrameBytesLayer,
134//! # FrameBytesService, FrameRouteService, RequestFrameLayer, RequestFrameService, RequestLayer,
135//! # ResponseService,
136//! # };
137//! # let frame_route = FrameRouteService::<(), Error>::builder()
138//! # .with_service(
139//! # RequestLayer::<MetadataRequest>::new().into_layer(ResponseService::new(|_, _| {
140//! # Ok(MetadataResponse::default()
141//! # .brokers(Some([].into()))
142//! # .topics(Some([].into()))
143//! # .cluster_id(Some("nisshi".into()))
144//! # .controller_id(Some(111))
145//! # .throttle_time_ms(Some(0))
146//! # .cluster_authorized_operations(Some(-1)))
147//! # })),
148//! # )
149//! # .and_then(|builder| builder.build())?;
150//! # let service = (
151//! # RequestFrameLayer,
152//! # FrameBytesLayer,
153//! # BytesLayer,
154//! # BytesFrameLayer::default(),
155//! # )
156//! # .into_layer(frame_route);
157//! let request = MetadataRequest::default()
158//! .topics(Some([].into()))
159//! .allow_auto_topic_creation(Some(false))
160//! .include_cluster_authorized_operations(Some(false))
161//! .include_topic_authorized_operations(Some(false));
162//!
163//! let response = service.serve(Context::default(), request).await?;
164//!
165//! assert_eq!(Some("nisshi".into()), response.cluster_id);
166//! # Ok(())
167//! # }
168//! ```
169//!
170//! The [`FrameRouteService`] automatically implements
171//! [`ApiVersionsRequest`][`nisshi_sans_io::ApiVersionsRequest`]
172//! with valid protocol ranges for all defined services. An
173//! [`ApiVersionsResponse`][`nisshi_sans_io::ApiVersionsResponse`] contains
174//! version information for both [`MetadataRequest`][`nisshi_sans_io::MetadataRequest`]
175//! and [`ApiVersionsRequest`][`nisshi_sans_io::ApiVersionsRequest`]:
176//!
177//! ```
178//! # use nisshi_service::Error;
179//! # #[tokio::main]
180//! # async fn main() -> Result<(), Error> {
181//! # use rama::{Context, Layer as _, Service as _};
182//! # use nisshi_sans_io::{ApiKey as _, ApiVersionsRequest, MetadataRequest, MetadataResponse};
183//! # use nisshi_service::{
184//! # BytesFrameLayer, BytesFrameService, BytesLayer, BytesService, FrameBytesLayer,
185//! # FrameBytesService, FrameRouteService, RequestFrameLayer, RequestFrameService, RequestLayer,
186//! # ResponseService,
187//! # };
188//! # let frame_route = FrameRouteService::<(), Error>::builder()
189//! # .with_service(
190//! # RequestLayer::<MetadataRequest>::new().into_layer(ResponseService::new(|_, _| {
191//! # Ok(MetadataResponse::default()
192//! # .brokers(Some([].into()))
193//! # .topics(Some([].into()))
194//! # .cluster_id(Some("nisshi".into()))
195//! # .controller_id(Some(111))
196//! # .throttle_time_ms(Some(0))
197//! # .cluster_authorized_operations(Some(-1)))
198//! # })),
199//! # )
200//! # .and_then(|builder| builder.build())?;
201//! # let service = (
202//! # RequestFrameLayer,
203//! # FrameBytesLayer,
204//! # BytesLayer,
205//! # BytesFrameLayer::default(),
206//! # )
207//! # .into_layer(frame_route);
208//! let response = service
209//! .serve(
210//! Context::default(),
211//! ApiVersionsRequest::default()
212//! .client_software_name(Some("abcba".into()))
213//! .client_software_version(Some("1.2321".into())),
214//! )
215//! .await?;
216//!
217//! let api_versions = response
218//! .api_keys
219//! .unwrap_or_default()
220//! .into_iter()
221//! .map(|api_version| api_version.api_key)
222//! .collect::<Vec<_>>();
223//!
224//! assert_eq!(2, api_versions.len());
225//! assert!(api_versions.contains(&ApiVersionsRequest::KEY));
226//! assert!(api_versions.contains(&MetadataRequest::KEY));
227//! # Ok(())
228//! # }
229//! ```
230
231use std::{
232 fmt, io,
233 net::SocketAddr,
234 ops::Range,
235 sync::{Arc, LazyLock, Mutex, PoisonError},
236 time::{Duration, SystemTime},
237};
238
239use nisshi_sans_io::{Body, Frame};
240use opentelemetry::{
241 InstrumentationScope, KeyValue, global,
242 metrics::{Counter, Histogram, Meter},
243};
244use opentelemetry_semantic_conventions::SCHEMA_URL;
245use rama::{Context, Layer, Service};
246use rand::{prelude::*, rngs::SmallRng};
247use tokio::{net::lookup_host, sync::oneshot, task::JoinError, time::sleep};
248use tracing::{debug, instrument};
249use url::Url;
250
251mod api;
252mod channel;
253mod consumer;
254mod frame;
255mod stream;
256
257pub use api::{ApiVersionsService, FrameRouteBuilder, FrameRouteService};
258
259pub use channel::{
260 ChannelFrameLayer, ChannelFrameService, FrameChannelService, FrameReceiver, FrameSender,
261 bounded_channel,
262};
263
264pub use consumer::{ConsumerGroupLayer, ConsumerGroupService};
265
266pub use frame::{
267 BodyRequestLayer, BytesFrameLayer, BytesFrameService, FrameApiKeyMatcher, FrameBodyLayer,
268 FrameBytesLayer, FrameBytesService, FrameRequestLayer, FrameService, RequestApiKeyMatcher,
269 RequestFrameLayer, RequestFrameService, RequestLayer, ResponseService,
270};
271
272pub use stream::{
273 BytesLayer, BytesService, BytesTcpService, TcpBytesLayer, TcpBytesService, TcpContext,
274 TcpContextLayer, TcpContextService, TcpListenerLayer,
275};
276
277#[derive(Clone, Debug, thiserror::Error)]
278pub enum Error {
279 Auth(#[from] nisshi_auth::Error),
280 DuplicateRoute(i16),
281 FrameTooBig(usize),
282 Io(Arc<io::Error>),
283 Join(Arc<JoinError>),
284 Message(String),
285 OneshotRecv(oneshot::error::RecvError),
286 Poison,
287 Parse(#[from] url::ParseError),
288 Protocol(#[from] nisshi_sans_io::Error),
289 UnableToSend(Box<Frame>),
290 UnknownHost(Url),
291 UnknownServiceBody(Box<Body>),
292 UnknownServiceFrame(Box<Frame>),
293}
294
295impl fmt::Display for Error {
296 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297 write!(f, "{self:?}")
298 }
299}
300
301impl From<JoinError> for Error {
302 fn from(value: JoinError) -> Self {
303 Self::Join(Arc::new(value))
304 }
305}
306
307impl From<io::Error> for Error {
308 fn from(value: io::Error) -> Self {
309 Self::Io(Arc::new(value))
310 }
311}
312
313impl<T> From<PoisonError<T>> for Error {
314 fn from(_value: PoisonError<T>) -> Self {
315 Self::Poison
316 }
317}
318
319fn frame_length(encoded: [u8; 4]) -> usize {
320 i32::from_be_bytes(encoded) as usize + encoded.len()
321}
322
323pub(crate) static METER: LazyLock<Meter> = LazyLock::new(|| {
324 global::meter_with_scope(
325 InstrumentationScope::builder(env!("CARGO_PKG_NAME"))
326 .with_version(env!("CARGO_PKG_VERSION"))
327 .with_schema_url(SCHEMA_URL)
328 .build(),
329 )
330});
331
332/// Return the socket address for a given URL
333///
334/// Recording DNS lookup timings in the `DNS_LOOKUP_DURATION` histogram.
335///
336/// ```rust
337/// # use nisshi_service::{Error, host_port};
338/// # use url::Url;
339/// # use std::net::Ipv4Addr;
340/// # #[tokio::main]
341/// # async fn main() -> Result<(), Error> {
342/// let earl = Url::parse("tcp://localhost:9092")?;
343/// let sock_addr = host_port(earl).await?;
344/// assert_eq!(sock_addr.ip(), Ipv4Addr::new(127, 0, 0, 1));
345/// assert_eq!(sock_addr.port(), 9092);
346/// # Ok(())
347/// # }
348/// ```
349pub async fn host_port(url: Url) -> Result<SocketAddr, Error> {
350 if let Some(host) = url.host_str()
351 && let Some(port) = url.port()
352 {
353 let attributes = [KeyValue::new("url", url.to_string())];
354 let start = SystemTime::now();
355
356 let mut addresses = lookup_host(format!("{host}:{port}"))
357 .await
358 .inspect(|_| {
359 DNS_LOOKUP_DURATION.record(
360 start
361 .elapsed()
362 .map_or(0, |duration| duration.as_millis() as u64),
363 &attributes,
364 )
365 })?
366 .filter(|socket_addr| matches!(socket_addr, SocketAddr::V4(_)));
367
368 if let Some(socket_addr) = addresses.next().inspect(|socket_addr| debug!(?socket_addr)) {
369 return Ok(socket_addr);
370 }
371 }
372
373 Err(Error::UnknownHost(url))
374}
375
376pub(crate) static DNS_LOOKUP_DURATION: LazyLock<Histogram<u64>> = LazyLock::new(|| {
377 METER
378 .u64_histogram("dns_lookup_duration")
379 .with_unit("ms")
380 .with_description("DNS lookup latencies")
381 .build()
382});
383
384pub(crate) static REQUEST_SIZE: LazyLock<Histogram<u64>> = LazyLock::new(|| {
385 METER
386 .u64_histogram("nisshi_request_size")
387 .with_unit("By")
388 .with_description("The API request size in bytes")
389 .build()
390});
391
392pub(crate) static RESPONSE_SIZE: LazyLock<Histogram<u64>> = LazyLock::new(|| {
393 METER
394 .u64_histogram("nisshi_response_size")
395 .with_unit("By")
396 .with_description("The API response size in bytes")
397 .build()
398});
399
400pub(crate) static REQUEST_DURATION: LazyLock<Histogram<u64>> = LazyLock::new(|| {
401 METER
402 .u64_histogram("nisshi_request_duration")
403 .with_unit("ms")
404 .with_description("The API request latencies in milliseconds")
405 .build()
406});
407
408pub(crate) static API_REQUESTS: LazyLock<Counter<u64>> = LazyLock::new(|| {
409 METER
410 .u64_counter("nisshi_api_requests")
411 .with_description("The number of API requests made")
412 .build()
413});
414
415pub(crate) static API_ERRORS: LazyLock<Counter<u64>> = LazyLock::new(|| {
416 METER
417 .u64_counter("nisshi_api_errors")
418 .with_description("The number of API errors")
419 .build()
420});
421
422pub(crate) static BYTES_SENT: LazyLock<Counter<u64>> = LazyLock::new(|| {
423 METER
424 .u64_counter("nisshi_bytes_sent")
425 .with_description("The number of bytes sent")
426 .build()
427});
428
429pub(crate) static BYTES_RECEIVED: LazyLock<Counter<u64>> = LazyLock::new(|| {
430 METER
431 .u64_counter("nisshi_bytes_received")
432 .with_description("The number of bytes received")
433 .build()
434});
435
436#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
437pub struct LatencyIntroducingLayer {
438 seed: u64,
439 latency: Range<u64>,
440}
441
442impl LatencyIntroducingLayer {
443 pub fn with_seed(self, seed: u64) -> Self {
444 Self { seed, ..self }
445 }
446
447 pub fn with_latency_millis(self, latency: Range<u64>) -> Self {
448 Self { latency, ..self }
449 }
450}
451
452impl<S> Layer<S> for LatencyIntroducingLayer {
453 type Service = LatencyIntroducingService<S>;
454
455 fn layer(&self, inner: S) -> Self::Service {
456 Self::Service::new(inner)
457 .with_seed(self.seed)
458 .with_latency(self.latency.clone())
459 }
460}
461
462#[derive(Clone)]
463pub struct LatencyIntroducingService<S> {
464 inner: S,
465 rng: Arc<Mutex<SmallRng>>,
466 latency: Range<u64>,
467}
468
469impl<S> fmt::Debug for LatencyIntroducingService<S> {
470 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
471 f.debug_struct(stringify!(LatencyIntroducingService))
472 .field("latency", &self.latency)
473 .finish_non_exhaustive()
474 }
475}
476
477impl<S> LatencyIntroducingService<S> {
478 pub fn new(inner: S) -> Self {
479 Self {
480 inner,
481 rng: Arc::new(Mutex::new(SmallRng::seed_from_u64(0))),
482 latency: 50..150,
483 }
484 }
485
486 pub fn with_seed(self, seed: u64) -> Self {
487 Self {
488 rng: Arc::new(Mutex::new(SmallRng::seed_from_u64(seed))),
489 ..self
490 }
491 }
492
493 pub fn with_latency(self, latency: Range<u64>) -> Self {
494 Self { latency, ..self }
495 }
496
497 #[instrument(skip_all)]
498 async fn introduce_latency(&self) -> Result<(), Error> {
499 let latency = self
500 .rng
501 .lock()
502 .map(|mut rng| rng.random_range(self.latency.clone()))
503 .map(Duration::from_millis)
504 .inspect(|latency| debug!(?latency))?;
505
506 sleep(latency).await;
507
508 Ok(())
509 }
510}
511
512impl<S, Q, State> Service<State, Q> for LatencyIntroducingService<S>
513where
514 S: Service<State, Q>,
515 Q: Send + 'static,
516 S::Error: From<Error>,
517 State: Send + Sync + 'static,
518{
519 type Response = S::Response;
520
521 type Error = S::Error;
522
523 #[instrument(skip(ctx, req))]
524 async fn serve(&self, ctx: Context<State>, req: Q) -> Result<Self::Response, Self::Error> {
525 self.introduce_latency().await?;
526 let result = self.inner.serve(ctx, req).await;
527 self.introduce_latency().await?;
528 result
529 }
530}