Skip to content

Интеграция с Tower

Tower — это экосистема модульных компонентов для построения надежных клиент-серверных приложений в Rust. Axum глубоко интегрирован с Tower, что обеспечивает мощные возможности для композиции middleware и создания гибких HTTP-сервисов. В этом разделе мы рассмотрим, как Axum работает с Tower и как эффективно использовать эту интеграцию.

Основные концепции Tower

Архитектура Tower

Tower построен вокруг трех ключевых абстракций:

  1. Service — это центральный типаж (trait) в Tower, который определяет сущность, способную обрабатывать запросы:
rust
pub trait Service<Request> {
    type Response;
    type Error;
    type Future: Future<Output = Result<Self::Response, Self::Error>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>;
    fn call(&mut self, req: Request) -> Self::Future;
}
  1. Layer — типаж, который применяет middleware к сервису:
rust
pub trait Layer<S> {
    type Service;
    fn layer(&self, inner: S) -> Self::Service;
}
  1. MakeService — фабрика, создающая сервисы:
rust
pub trait MakeService<Target, Request> {
    type Response;
    type Error;
    type Service: Service<Request, Response = Self::Response, Error = Self::Error>;
    type MakeError;
    type Future: Future<Output = Result<Self::Service, Self::MakeError>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::MakeError>>;
    fn make_service(&mut self, target: Target) -> Self::Future;
}

Типы сервисов в Tower

Tower предоставляет несколько типов сервисов, которые можно использовать для построения приложений:

  • ServiceBuilder — удобный интерфейс для композиции слоев
  • BoxService — тип-обертка, который стирает конкретный тип сервиса
  • Buffer — буферизует запросы, когда внутренний сервис не готов
  • Timeout — ограничивает время выполнения запроса
  • Retry — повторяет запросы при возникновении ошибок
  • Rate — ограничивает скорость обработки запросов
  • Load — распределяет нагрузку между несколькими сервисами

Интеграция Axum с Tower

Router как реализация Service

Центральный компонент Axum — Router — реализует типаж Service. Это позволяет применять к нему Tower middleware и интегрировать его с другими компонентами экосистемы Tower.

rust
use axum::{Router, routing::get};
use tower::ServiceBuilder;
use tower_http::trace::TraceLayer;

let app = Router::new()
    .route("/", get(handler));

// Применение Tower middleware с помощью ServiceBuilder
let app = ServiceBuilder::new()
    .layer(TraceLayer::new_for_http())
    .service(app);

Трансформация Router в MakeService

Для работы с HTTP-сервером Axum необходимо преобразовать Router в MakeService. Для этого используется метод .into_make_service():

rust
use axum::{Router, routing::get};
use std::net::SocketAddr;

let app = Router::new()
    .route("/", get(handler));

let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
    .serve(app.into_make_service())
    .await
    .unwrap();

Этот метод создает MakeService, который для каждого соединения создает новый экземпляр Router.

Применение Tower middleware

В Axum есть несколько способов применения Tower middleware:

  1. Метод .layer() — добавляет слой к роутеру:
rust
use axum::{Router, routing::get};
use tower_http::compression::CompressionLayer;

let app = Router::new()
    .route("/", get(handler))
    .layer(CompressionLayer::new());
  1. ServiceBuilder — более гибкий подход для композиции нескольких слоев:
rust
use axum::{Router, routing::get};
use tower::ServiceBuilder;
use tower_http::{
    compression::CompressionLayer,
    trace::TraceLayer,
    timeout::TimeoutLayer,
};
use std::time::Duration;

let app = Router::new()
    .route("/", get(handler));

let app = ServiceBuilder::new()
    .layer(TraceLayer::new_for_http())
    .layer(CompressionLayer::new())
    .layer(TimeoutLayer::new(Duration::from_secs(30)))
    .service(app);

Использование tower-http

Пакет tower-http содержит множество полезных HTTP-специфичных middleware для Tower, которые можно использовать с Axum:

rust
use axum::{Router, routing::get};
use tower::ServiceBuilder;
use tower_http::{
    compression::CompressionLayer,
    trace::TraceLayer,
    cors::CorsLayer,
    limit::RequestBodyLimitLayer,
    catch_panic::CatchPanicLayer,
    timeout::TimeoutLayer,
    add_extension::AddExtensionLayer,
    sensitive_headers::SetSensitiveHeadersLayer,
};
use std::time::Duration;

let app = Router::new()
    .route("/", get(handler));

let app = ServiceBuilder::new()
    // Трассировка запросов
    .layer(TraceLayer::new_for_http())
    // Перехват паники в обработчиках
    .layer(CatchPanicLayer::new())
    // Таймаут для запросов
    .layer(TimeoutLayer::new(Duration::from_secs(30)))
    // CORS
    .layer(CorsLayer::permissive())
    // Ограничение размера тела запроса
    .layer(RequestBodyLimitLayer::new(1024 * 1024)) // 1 MB
    // Скрытие чувствительных заголовков из логов
    .layer(SetSensitiveHeadersLayer::new(vec!["authorization", "cookie"]))
    // Добавление данных в расширения запроса
    .layer(AddExtensionLayer::new(MyAppState::default()))
    // Сжатие ответов
    .layer(CompressionLayer::new())
    .service(app);

Создание собственных Tower middleware

Вы можете создавать собственные Tower middleware путем реализации типажей Layer и Service:

rust
use std::{
    pin::Pin,
    future::{Future, Ready},
    task::{Context, Poll},
    marker::PhantomData,
};
use tower::{Layer, Service};
use axum::http::{Request, Response};

// Определение Layer
#[derive(Clone)]
struct LoggingLayer {
    target: &'static str,
}

impl LoggingLayer {
    pub fn new(target: &'static str) -> Self {
        Self { target }
    }
}

// Реализация Layer
impl<S> Layer<S> for LoggingLayer {
    type Service = LoggingMiddleware<S>;

    fn layer(&self, inner: S) -> Self::Service {
        LoggingMiddleware {
            inner,
            target: self.target,
        }
    }
}

// Определение Service
#[derive(Clone)]
struct LoggingMiddleware<S> {
    inner: S,
    target: &'static str,
}

// Реализация Service
impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for LoggingMiddleware<S>
where
    S: Service<Request<ReqBody>, Response = Response<ResBody>>,
    S::Future: Send + 'static,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, request: Request<ReqBody>) -> Self::Future {
        println!("[{}] {} {}", self.target, request.method(), request.uri());
        
        let start = std::time::Instant::now();
        let future = self.inner.call(request);
        
        Box::pin(async move {
            let response = future.await?;
            let duration = start.elapsed();
            
            println!("[{}] Completed in {:?}", self.target, duration);
            
            Ok(response)
        })
    }
}

// Использование
let app = Router::new()
    .route("/", get(handler))
    .layer(LoggingLayer::new("api"));

Комбинирование Tower middleware

Tower позволяет комбинировать middleware различными способами:

Последовательное применение слоев

rust
use tower::ServiceBuilder;

let app = ServiceBuilder::new()
    .layer(layer1)
    .layer(layer2)
    .layer(layer3)
    .service(app);

Условное применение слоев

rust
use tower::ServiceBuilder;

let mut builder = ServiceBuilder::new();

if config.tracing_enabled {
    builder = builder.layer(tower_http::trace::TraceLayer::new_for_http());
}

if config.compression_enabled {
    builder = builder.layer(tower_http::compression::CompressionLayer::new());
}

let app = builder.service(app);

Оптимизация производительности

Tower предоставляет middleware для оптимизации производительности:

rust
use tower::ServiceBuilder;
use tower::limit::{RateLimitLayer, ConcurrencyLimitLayer};
use tower::buffer::BufferLayer;
use tower::load_shed::LoadShedLayer;
use std::time::Duration;

let app = ServiceBuilder::new()
    // Ограничивает количество одновременных запросов
    .layer(ConcurrencyLimitLayer::new(100))
    // Ограничивает скорость запросов
    .layer(RateLimitLayer::new(50, Duration::from_secs(1)))
    // Отклоняет запросы при перегрузке
    .layer(LoadShedLayer::new())
    // Буферизует запросы
    .layer(BufferLayer::new(1024))
    .service(app);

Поддержка сервисов не-HTTP типов

Tower не ограничивается HTTP, и вы можете использовать его типы для любых клиент-серверных абстракций:

rust
use tower::{Service, ServiceBuilder, buffer::BufferLayer, timeout::TimeoutLayer};
use std::time::Duration;
use futures::future::BoxFuture;

// Определяем собственный тип запроса
struct CustomRequest {
    id: u64,
    payload: Vec<u8>,
}

// Определяем сервис для обработки этих запросов
struct CustomService;

impl Service<CustomRequest> for CustomService {
    type Response = Vec<u8>;
    type Error = anyhow::Error;
    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;

    fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        Poll::Ready(Ok(()))
    }

    fn call(&mut self, req: CustomRequest) -> Self::Future {
        println!("Processing request with ID: {}", req.id);
        
        Box::pin(async move {
            // Обработка запроса
            Ok(req.payload)
        })
    }
}

// Применяем Tower middleware
let service = ServiceBuilder::new()
    .layer(TimeoutLayer::new(Duration::from_secs(5)))
    .layer(BufferLayer::new(100))
    .service(CustomService);

Лучшие практики работы с Tower

Типизация и стирание типов

Tower сильно полагается на статическую типизацию, что может приводить к сложным типам при композиции слоев. Для упрощения можно использовать стирание типов:

rust
use tower::util::ServiceExt;
use tower::BoxService;

// Создаем сервис со стертым типом
let app: BoxService<Request<Body>, Response<Body>, hyper::Error> = 
    app.map_response(|res| res.map(Body::from))
       .boxed();

Обработка ошибок

Обработка ошибок в Tower middleware может быть сложной. Рекомендуется использовать типаж Map для преобразования ошибок:

rust
use tower::util::ServiceExt;

let app = app
    .map_err(|e| {
        eprintln!("Service error: {:?}", e);
        axum::http::StatusCode::INTERNAL_SERVER_ERROR
    });

Тестирование сервисов

Tower облегчает тестирование HTTP-сервисов без запуска реального сервера:

rust
use tower::ServiceExt;
use axum::http::{Request, StatusCode};
use axum::body::Body;

#[tokio::test]
async fn test_service() {
    let app = Router::new()
        .route("/", get(|| async { "Hello, World!" }))
        .layer(tower_http::trace::TraceLayer::new_for_http());
    
    // Создаем запрос
    let request = Request::builder()
        .uri("/")
        .body(Body::empty())
        .unwrap();
    
    // Тестируем как сервис напрямую
    let response = app
        .oneshot(request)
        .await
        .unwrap();
    
    assert_eq!(response.status(), StatusCode::OK);
}

Заключение

Интеграция Axum с Tower предоставляет мощный и гибкий фундамент для построения HTTP-сервисов. Используя Tower, вы получаете доступ к богатой экосистеме middleware, которые могут быть скомпонованы различными способами для достижения нужной функциональности.

Эта интеграция дает Axum несколько ключевых преимуществ:

  1. Модульность — каждый middleware решает одну конкретную задачу и может быть применен независимо
  2. Компонуемость — middleware можно комбинировать для создания сложных сервисов
  3. Типобезопасность — система типов Rust гарантирует корректность композиции
  4. Производительность — Tower оптимизирован для высокой пропускной способности
  5. Расширяемость — возможность легко создавать собственные middleware

Эффективное использование Tower позволяет создавать надежные и высокопроизводительные веб-приложения в Axum, способные масштабироваться до высоких нагрузок.