Skip to content

Commit

Permalink
refactor(api): [#174] Axum API scaffolding
Browse files Browse the repository at this point in the history
Basic changes needed to run the Axum API implementation in parallel with the current one with ActixWeb.

For the time being, we will be only useinf the Auxm implementation for testing until all endpoints are migrated.
  • Loading branch information
josecelano committed Jun 8, 2023
1 parent 7bcf20e commit d08f70e
Show file tree
Hide file tree
Showing 20 changed files with 398 additions and 94 deletions.
45 changes: 13 additions & 32 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
use std::net::SocketAddr;
use std::sync::Arc;

use actix_cors::Cors;
use actix_web::dev::Server;
use actix_web::{middleware, web, App, HttpServer};
use log::info;
use tokio::task::JoinHandle;

use crate::auth::Authentication;
use crate::bootstrap::logging;
Expand All @@ -21,16 +18,18 @@ use crate::services::torrent::{
use crate::services::user::{self, DbBannedUserList, DbUserProfileRepository, DbUserRepository};
use crate::services::{proxy, settings, torrent};
use crate::tracker::statistics_importer::StatisticsImporter;
use crate::{mailer, routes, tracker};
use crate::web::api::{start, Implementation};
use crate::{mailer, tracker};

pub struct Running {
pub api_server: Server,
pub socket_address: SocketAddr,
pub api_socket_addr: SocketAddr,
pub actix_web_api_server: Option<JoinHandle<std::result::Result<(), std::io::Error>>>,
pub axum_api_server: Option<JoinHandle<std::result::Result<(), std::io::Error>>>,
pub tracker_data_importer_handle: tokio::task::JoinHandle<()>,
}

#[allow(clippy::too_many_lines)]
pub async fn run(configuration: Configuration) -> Running {
pub async fn run(configuration: Configuration, api_implementation: &Implementation) -> Running {
logging::setup();

let configuration = Arc::new(configuration);
Expand All @@ -42,6 +41,7 @@ pub async fn run(configuration: Configuration) -> Running {

let database_connect_url = settings.database.connect_url.clone();
let torrent_info_update_interval = settings.tracker_statistics_importer.torrent_info_update_interval;
let net_ip = "0.0.0.0".to_string();
let net_port = settings.net.port;

// IMPORTANT: drop settings before starting server to avoid read locks that
Expand Down Expand Up @@ -155,33 +155,14 @@ pub async fn run(configuration: Configuration) -> Running {
}
});

// Start main API server
// Start API server

// todo: get IP from settings
let ip = "0.0.0.0".to_string();

let server = HttpServer::new(move || {
App::new()
.wrap(Cors::permissive())
.app_data(web::Data::new(app_data.clone()))
.wrap(middleware::Logger::default())
.configure(routes::init)
})
.bind((ip, net_port))
.expect("can't bind server to socket address");

let socket_address = server.addrs()[0];

let running_server = server.run();

let starting_message = format!("Listening on http://{socket_address}");
info!("{}", starting_message);
// Logging could be disabled or redirected to file. So print to stdout too.
println!("{starting_message}");
let running_api = start(app_data, &net_ip, net_port, api_implementation).await;

Running {
api_server: running_server,
socket_address,
api_socket_addr: running_api.socket_addr,
actix_web_api_server: running_api.actix_web_api_server,
axum_api_server: running_api.axum_api_server,
tracker_data_importer_handle: tracker_statistics_importer_handle,
}
}
16 changes: 13 additions & 3 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
use torrust_index_backend::app;
use torrust_index_backend::bootstrap::config::init_configuration;
use torrust_index_backend::web::api::Implementation;

#[actix_web::main]
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
let configuration = init_configuration().await;

let app = app::run(configuration).await;
// todo: we are migrating from actix-web to axum, so we need to keep both
// implementations for a while. For production we only use ActixWeb.
// Once the Axum implementation is finished and stable, we can switch to it
// and remove the ActixWeb implementation.
let api_implementation = Implementation::ActixWeb;

app.api_server.await
let app = app::run(configuration, &api_implementation).await;

match api_implementation {
Implementation::ActixWeb => app.actix_web_api_server.unwrap().await.expect("the API server was dropped"),
Implementation::Axum => app.axum_api_server.unwrap().await.expect("the Axum API server was dropped"),
}
}
5 changes: 5 additions & 0 deletions src/services/about.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ use crate::routes::API_VERSION;

#[must_use]
pub fn index_page() -> String {
page()
}

#[must_use]
pub fn page() -> String {
format!(
r#"
<html>
Expand Down
75 changes: 75 additions & 0 deletions src/web/api/actix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use std::net::SocketAddr;
use std::sync::Arc;

use actix_cors::Cors;
use actix_web::{middleware, web, App, HttpServer};
use log::info;
use tokio::sync::oneshot::{self, Sender};

use super::Running;
use crate::common::AppData;
use crate::routes;
use crate::web::api::ServerStartedMessage;

/// Starts the API server with `ActixWeb`.
///
/// # Panics
///
/// Panics if the API server can't be started.
pub async fn start(app_data: Arc<AppData>, net_ip: &str, net_port: u16) -> Running {
let config_socket_addr: SocketAddr = format!("{net_ip}:{net_port}")
.parse()
.expect("API server socket address to be valid.");

let (tx, rx) = oneshot::channel::<ServerStartedMessage>();

// Run the API server
let join_handle = tokio::spawn(async move {
info!("Starting API server with net config: {} ...", config_socket_addr);

let server_future = start_server(config_socket_addr, app_data.clone(), tx);

let _ = server_future.await;

Ok(())
});

// Wait until the API server is running
let bound_addr = match rx.await {
Ok(msg) => msg.socket_addr,
Err(e) => panic!("API server start. The API server was dropped: {e}"),
};

info!("API server started");

Running {
socket_addr: bound_addr,
actix_web_api_server: Some(join_handle),
axum_api_server: None,
}
}

fn start_server(
config_socket_addr: SocketAddr,
app_data: Arc<AppData>,
tx: Sender<ServerStartedMessage>,
) -> actix_web::dev::Server {
let server = HttpServer::new(move || {
App::new()
.wrap(Cors::permissive())
.app_data(web::Data::new(app_data.clone()))
.wrap(middleware::Logger::default())
.configure(routes::init)
})
.bind(config_socket_addr)
.expect("can't bind server to socket address");

let bound_addr = server.addrs()[0];

info!("API server listening on http://{}", bound_addr);

tx.send(ServerStartedMessage { socket_addr: bound_addr })
.expect("the API server should not be dropped");

server.run()
}
76 changes: 76 additions & 0 deletions src/web/api/axum.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use std::net::SocketAddr;
use std::sync::Arc;

use futures::Future;
use log::info;
use tokio::sync::oneshot::{self, Sender};

use super::v1::routes::router;
use super::{Running, ServerStartedMessage};
use crate::common::AppData;

/// Starts the API server with `Axum`.
///
/// # Panics
///
/// Panics if the API server can't be started.
pub async fn start(app_data: Arc<AppData>, net_ip: &str, net_port: u16) -> Running {
let config_socket_addr: SocketAddr = format!("{net_ip}:{net_port}")
.parse()
.expect("API server socket address to be valid.");

let (tx, rx) = oneshot::channel::<ServerStartedMessage>();

// Run the API server
let join_handle = tokio::spawn(async move {
info!("Starting API server with net config: {} ...", config_socket_addr);

let handle = start_server(config_socket_addr, app_data.clone(), tx);

if let Ok(()) = handle.await {
info!("API server stopped");
}

Ok(())
});

// Wait until the API server is running
let bound_addr = match rx.await {
Ok(msg) => msg.socket_addr,
Err(e) => panic!("API server start. The API server was dropped: {e}"),
};

Running {
socket_addr: bound_addr,
actix_web_api_server: None,
axum_api_server: Some(join_handle),
}
}

fn start_server(
config_socket_addr: SocketAddr,
app_data: Arc<AppData>,
tx: Sender<ServerStartedMessage>,
) -> impl Future<Output = hyper::Result<()>> {
let tcp_listener = std::net::TcpListener::bind(config_socket_addr).expect("tcp listener to bind to a socket address");

let bound_addr = tcp_listener
.local_addr()
.expect("tcp listener to be bound to a socket address.");

info!("API server listening on http://{}", bound_addr);

let app = router(app_data);

let server = axum::Server::from_tcp(tcp_listener)
.expect("a new server from the previously created tcp listener.")
.serve(app.into_make_service_with_connect_info::<SocketAddr>());

tx.send(ServerStartedMessage { socket_addr: bound_addr })
.expect("the API server should not be dropped");

server.with_graceful_shutdown(async move {
tokio::signal::ctrl_c().await.expect("Failed to listen to shutdown signal.");
info!("Stopping API server on http://{} ...", bound_addr);
})
}
47 changes: 47 additions & 0 deletions src/web/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,51 @@
//! Currently, the API has only one version: `v1`.
//!
//! Refer to the [`v1`](crate::web::api::v1) module for more information.
pub mod actix;
pub mod axum;
pub mod v1;

use std::net::SocketAddr;
use std::sync::Arc;

use tokio::task::JoinHandle;

use crate::common::AppData;
use crate::web::api;

/// API implementations.
pub enum Implementation {
/// API implementation with Actix Web.
ActixWeb,
/// API implementation with Axum.
Axum,
}

/// The running API server.
pub struct Running {
/// The socket address the API server is listening on.
pub socket_addr: SocketAddr,
/// The API server when using Actix Web.
pub actix_web_api_server: Option<JoinHandle<Result<(), std::io::Error>>>,
/// The handle for the running API server task when using Axum.
pub axum_api_server: Option<JoinHandle<Result<(), std::io::Error>>>,
}

#[must_use]
#[derive(Debug)]
pub struct ServerStartedMessage {
pub socket_addr: SocketAddr,
}

/// Starts the API server.
///
/// We are migrating the API server from Actix Web to Axum. While the migration
/// is in progress, we will keep both implementations, running the Axum one only
/// for testing purposes.
#[must_use]
pub async fn start(app_data: Arc<AppData>, net_ip: &str, net_port: u16, implementation: &Implementation) -> api::Running {
match implementation {
Implementation::ActixWeb => actix::start(app_data, net_ip, net_port).await,
Implementation::Axum => axum::start(app_data, net_ip, net_port).await,
}
}
20 changes: 20 additions & 0 deletions src/web/api/v1/contexts/about/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//! API handlers for the the [`about`](crate::web::api::v1::contexts::about) API
//! context.
use std::sync::Arc;

use axum::extract::State;
use axum::http::{header, StatusCode};
use axum::response::{IntoResponse, Response};

use crate::common::AppData;
use crate::services::about;

#[allow(clippy::unused_async)]
pub async fn about_page_handler(State(_app_data): State<Arc<AppData>>) -> Response {
(
StatusCode::OK,
[(header::CONTENT_TYPE, "text/html; charset=utf-8")],
about::page(),
)
.into_response()
}
2 changes: 2 additions & 0 deletions src/web/api/v1/contexts/about/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,5 @@
//! </footer>
//! </html>
//! ```
pub mod handlers;
pub mod routes;
15 changes: 15 additions & 0 deletions src/web/api/v1/contexts/about/routes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//! API routes for the [`about`](crate::web::api::v1::contexts::about) API context.
//!
//! Refer to the [API endpoint documentation](crate::web::api::v1::contexts::about).
use std::sync::Arc;

use axum::routing::get;
use axum::Router;

use super::handlers::about_page_handler;
use crate::common::AppData;

/// It adds the routes to the router for the [`about`](crate::web::api::v1::contexts::about) API context.
pub fn add(prefix: &str, router: Router, app_data: Arc<AppData>) -> Router {
router.route(&format!("{prefix}/about"), get(about_page_handler).with_state(app_data))
}
1 change: 1 addition & 0 deletions src/web/api/v1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
//! information.
pub mod auth;
pub mod contexts;
pub mod routes;
22 changes: 22 additions & 0 deletions src/web/api/v1/routes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//! Route initialization for the v1 API.
use std::sync::Arc;

use axum::Router;

use super::contexts::about;
use crate::common::AppData;

/// Add all API routes to the router.
#[allow(clippy::needless_pass_by_value)]
pub fn router(app_data: Arc<AppData>) -> Router {
let router = Router::new();

add(router, app_data)
}

/// Add the routes for the v1 API.
fn add(router: Router, app_data: Arc<AppData>) -> Router {
let v1_prefix = "/v1".to_string();

about::routes::add(&v1_prefix, router, app_data)
}
Loading

0 comments on commit d08f70e

Please sign in to comment.