|
| 1 | +--- |
| 2 | +title: "Context Management" |
| 3 | +linkTitle: "Context" |
| 4 | +weight: 4 |
| 5 | +description: "Deep dive into Monolake's io_uring-based runtime and performance characteristics compared to traditional event-based runtimes" |
| 6 | +--- |
| 7 | + |
| 8 | +# Context Management with `certain_map` |
| 9 | + |
| 10 | +In a service-oriented architecture, managing the context data that flows between different services is a critical aspect of the system design. The [`certain_map`](https://docs.rs/certain-map/latest/certain_map/) crate provides a powerful way to define and work with typed context data, ensuring the existence of required information at compile-time. |
| 11 | + |
| 12 | +## The Problem `certain_map` Solves |
| 13 | + |
| 14 | +When building modular services, it's common to have indirect data dependencies between components. For example, a downstream service may require information that was originally provided in an upstream request, but the intermediate services don't directly use that data. Traditionally, this would involve passing all potentially relevant data through the request/response types, which can quickly become unwieldy and error-prone. |
| 15 | + |
| 16 | +Alternatively, you might use a `HashMap` to manage the context data, but this approach has a significant drawback: you cannot ensure at compile-time that the required key-value pairs have been set when the data is read. This can lead to unnecessary error handling branches or even panics in your program. |
| 17 | + |
| 18 | +## How `certain_map` Helps |
| 19 | + |
| 20 | +The `certain_map` crate solves this problem by providing a typed map that ensures the existence of specific items at compile-time. When you define a `Context` struct using `certain_map`, the compiler will enforce that certain fields are present, preventing runtime errors and simplifying the implementation of your services. |
| 21 | + |
| 22 | +Here's an example of how you might set up the context for your project: |
| 23 | + |
| 24 | +```rust |
| 25 | +certain_map::certain_map! { |
| 26 | + #[derive(Debug, Clone)] |
| 27 | + #[empty(EmptyContext)] |
| 28 | + #[full(FullContext)] |
| 29 | + pub struct Context { |
| 30 | + peer_addr: PeerAddr, |
| 31 | + remote_addr: Option<RemoteAddr>, |
| 32 | + } |
| 33 | +} |
| 34 | +``` |
| 35 | + |
| 36 | +In this example, the `Context` struct has two fields: `peer_addr` of type `PeerAddr`, and `remote_addr` of type `Option<RemoteAddr>`. The `#[empty(EmptyContext)]` and `#[full(FullContext)]` attributes define the type aliases for the empty and full versions of the context, respectively. |
| 37 | + |
| 38 | +The key benefits of using `certain_map` for your context management are: |
| 39 | + |
| 40 | +1. **Compile-time Guarantees**: The compiler will ensure that the necessary fields are present in the `Context` struct, preventing runtime errors and simplifying the implementation of your services. |
| 41 | + |
| 42 | +2. **Modularity and Composability**: By defining a clear context structure, you can more easily compose services together, as each service can specify the context data it requires using trait bounds. |
| 43 | + |
| 44 | +3. **Flexibility**: The `certain_map` crate provides a set of traits (`ParamSet`, `ParamRef`, `ParamTake`, etc.) that allow you to easily manipulate the context data, such as adding, removing, or modifying fields. |
| 45 | + |
| 46 | +4. **Reduced Boilerplate**: Instead of manually creating and managing structs to hold the context data, the `certain_map` crate generates the necessary code for you, reducing the amount of boilerplate in your project. |
| 47 | + |
| 48 | +## Using `certain_map` in Your Services |
| 49 | + |
| 50 | +Once you've defined your `Context` struct, you can use it in your services to ensure that the required data is available. For example, consider the following `UpstreamHandler` service: |
| 51 | + |
| 52 | +```rust |
| 53 | +impl<CX, B> Service<(Request<B>, CX)> for UpstreamHandler |
| 54 | +where |
| 55 | + CX: ParamRef<PeerAddr> + ParamMaybeRef<Option<RemoteAddr>>, |
| 56 | + B: Body<Data = Bytes, Error = HttpError>, |
| 57 | + HttpError: From<B::Error>, |
| 58 | +{ |
| 59 | + type Response = ResponseWithContinue<HttpBody>; |
| 60 | + type Error = Infallible; |
| 61 | + |
| 62 | + async fn call(&self, (mut req, ctx): (Request<B>, CX)) -> Result<Self::Response, Self::Error> { |
| 63 | + add_xff_header(req.headers_mut(), &ctx); |
| 64 | + #[cfg(feature = "tls")] |
| 65 | + if req.uri().scheme() == Some(&http::uri::Scheme::HTTPS) { |
| 66 | + return self.send_https_request(req).await; |
| 67 | + } |
| 68 | + self.send_http_request(req).await |
| 69 | + } |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +In this example, the `UpstreamHandler` service expects the `Context` to contain the `PeerAddr` and optionally the `RemoteAddr`. The trait bounds `ParamRef<PeerAddr>` and `ParamMaybeRef<Option<RemoteAddr>>` ensure that these fields are available at compile-time, preventing potential runtime errors. |
| 74 | + |
| 75 | +By using `certain_map` to manage your context data, you can improve the modularity, maintainability, and robustness of your service-oriented architecture. |
0 commit comments