Skip to main content

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}