Layer
A Layer is a crucial component in Shizuku that wraps around a Processor, enabling middleware-like functionality. It provides a clean way to handle cross-cutting concerns by intercepting and potentially modifying both the input and output of a Processor.
Conceptually, a Layer is a monad transformer that can perform operations before and after the execution of a Processor, without the Processor needing to be aware of these operations. This allows for a clear separation between business logic (in Processors) and cross-cutting concerns (in Layers).
Why Use Layers?
Layers offer several advantages in a functional microservice architecture:
- Separation of concerns - Keep cross-cutting concerns separate from business logic
- Composability - Layers can be stacked and combined in different orders
- Reusability - Common functionality like retry logic, logging, or metrics can be implemented once and reused
- Non-intrusive - Add functionality without modifying existing Processors
- Functional approach - Aligns with functional programming principles by treating operations as transformations
Core Layer Trait
The fundamental trait that defines a Layer is:
pub trait Layer<I, O, P: Processor<I, O>> { /// Wrap the processor and return the output. fn wrap<'wrapper, 'processor>( &'wrapper self, processor: &'processor P, input: I, ) -> impl Future<Output = O> + Send + 'wrapper + 'processor where I: 'wrapper + 'processor, 'processor: 'wrapper;}
Where:
I
- The input type the processor acceptsO
- The output type the processor producesP
- The Processor type being wrapped- The return type is an implementor of
Future
that isSend
and lives at least as long as the borrow of both the wrapper and processor
Built-in Layers
Shizuku provides several built-in Layers for common patterns:
RetryLayer
The RetryLayer
automatically retries a Processor’s execution when it encounters errors, with configurable retry limits:
pub struct RetryLayer { /// The maximum number of retries. pub max_retry: usize,}
impl<Input, Success, P> Layer<Input, Result<Success, Error>, P> for RetryLayerwhere P: Processor<Input, Result<Success, Error>> + Send + Sync, Input: Clone + Send + Sync,{ fn wrap<'w, 'p>( &'w self, processor: &'p P, input: Input, ) -> impl Future<Output = Result<Success, Error>> + Send + 'w + 'p where Input: 'w + 'p, 'p: 'w, { // Implementation details omitted for brevity }}
Implementation Guide
When implementing your own Layers, follow these best practices:
- Keep Layers focused - Each Layer should address a single concern
- Consider performance - Be mindful of cloning inputs or heavy operations
- Handle lifetimes carefully - Understand how lifetime parameters affect the Layer’s behavior
- Preserve type signatures - Layers should generally not change the input/output types unless necessary
- Make dependencies explicit - Any external dependencies should be fields of your Layer
Basic Implementation Example
Here’s how to implement a simple metrics-collecting Layer:
struct MetricsLayer { metrics_client: &'static MetricsClient,}
impl<Input, Output, P> Layer<Input, Output, P> for MetricsLayerwhere P: Processor<Input, Output> + Send + Sync, Input: Send + Sync, Output: Send + Sync,{ async fn wrap<'w, 'p>( &'w self, processor: &'p P, input: Input, ) -> Output { let processor_name = std::any::type_name::<P>(); let timer = self.metrics_client.start_timer(processor_name);
// Process the input let result = processor.process(input).await;
// Record duration timer.observe_duration();
// Increment success/failure counters match &result { Ok(_) => self.metrics_client.increment_success(processor_name), Err(_) => self.metrics_client.increment_failure(processor_name), }
result }}
Using Layers with FinalProcessor
While there is no separate FinalLayer
trait, the standard Layer
trait can be used with both Processor
and FinalProcessor
implementations. When working with a FinalProcessor
, the Layer would typically be part of the service construction and owned by the a structure that implements the FinalProcessor
.
struct MyService { processor: Arc<MyFinalProcessor>, retry_layer: RetryLayer,}
impl FinalProcessor<Input, Output> for MyService { async fn process(state: Arc<Self>, input: Input) -> Output { // Apply the layer to the processor state.retry_layer.wrap(&state.processor, input).await }}