From 140f1130171788bce6d82089828fcc61c485734d Mon Sep 17 00:00:00 2001 From: Egor Berezovskiy Date: Fri, 12 Jul 2024 10:20:10 +0200 Subject: [PATCH] editoast: refactor app health view --- .../editoast_models/src/db_connection_pool.rs | 11 +++- editoast/editoast_models/src/lib.rs | 1 + editoast/openapi.yaml | 60 +++++++++++++++++++ editoast/src/redis_utils.rs | 18 ++++-- editoast/src/views/mod.rs | 47 +++++++++++---- front/public/locales/en/errors.json | 5 ++ front/public/locales/fr/errors.json | 5 ++ 7 files changed, 131 insertions(+), 16 deletions(-) diff --git a/editoast/editoast_models/src/db_connection_pool.rs b/editoast/editoast_models/src/db_connection_pool.rs index 55e2eb1acaf..16d3b649c31 100644 --- a/editoast/editoast_models/src/db_connection_pool.rs +++ b/editoast/editoast_models/src/db_connection_pool.rs @@ -1,18 +1,20 @@ +use std::sync::Arc; + +use diesel::sql_query; use diesel::ConnectionError; use diesel::ConnectionResult; use diesel_async::pooled_connection::deadpool::Object; use diesel_async::pooled_connection::deadpool::Pool; - use diesel_async::pooled_connection::AsyncDieselConnectionManager; use diesel_async::pooled_connection::ManagerConfig; use diesel_async::AsyncPgConnection; +use diesel_async::RunQueryDsl; use futures::future::BoxFuture; use futures::Future; use futures_util::FutureExt as _; use openssl::ssl::SslConnector; use openssl::ssl::SslMethod; use openssl::ssl::SslVerifyMode; -use std::sync::Arc; use url::Url; #[cfg(feature = "testing")] @@ -328,6 +330,11 @@ impl DbConnectionPoolV2 { } } +pub async fn ping_database(conn: &mut DbConnection) -> Result<(), EditoastModelsError> { + sql_query("SELECT 1").execute(conn).await?; + Ok(()) +} + pub fn create_connection_pool( url: Url, max_size: usize, diff --git a/editoast/editoast_models/src/lib.rs b/editoast/editoast_models/src/lib.rs index cb07797edd9..d46f846dfff 100644 --- a/editoast/editoast_models/src/lib.rs +++ b/editoast/editoast_models/src/lib.rs @@ -4,6 +4,7 @@ mod db_connection_pool; mod error; pub use db_connection_pool::create_connection_pool; +pub use db_connection_pool::ping_database; pub use db_connection_pool::DbConnectionPoolV2; pub use error::EditoastModelsError; diff --git a/editoast/openapi.yaml b/editoast/openapi.yaml index cf72a2c1153..b7d1586b51d 100644 --- a/editoast/openapi.yaml +++ b/editoast/openapi.yaml @@ -3794,6 +3794,63 @@ components: enum: - STANDARD - MARECO + EditoastAppHealthErrorDatabase: + type: object + required: + - type + - status + - message + properties: + context: + type: object + message: + type: string + status: + type: integer + enum: + - 400 + type: + type: string + enum: + - editoast:app_health:Database + EditoastAppHealthErrorRedis: + type: object + required: + - type + - status + - message + properties: + context: + type: object + message: + type: string + status: + type: integer + enum: + - 400 + type: + type: string + enum: + - editoast:app_health:Redis + EditoastAppHealthErrorTimeout: + type: object + required: + - type + - status + - message + properties: + context: + type: object + message: + type: string + status: + type: integer + enum: + - 400 + type: + type: string + enum: + - editoast:app_health:Timeout EditoastAttachedErrorTrackNotFound: type: object required: @@ -4248,6 +4305,9 @@ components: - editoast:electrical_profiles:NotFound EditoastError: oneOf: + - $ref: '#/components/schemas/EditoastAppHealthErrorDatabase' + - $ref: '#/components/schemas/EditoastAppHealthErrorRedis' + - $ref: '#/components/schemas/EditoastAppHealthErrorTimeout' - $ref: '#/components/schemas/EditoastAttachedErrorTrackNotFound' - $ref: '#/components/schemas/EditoastAutoFixesEditoastErrorConflictingFixesOnSameObject' - $ref: '#/components/schemas/EditoastAutoFixesEditoastErrorFixTrialFailure' diff --git a/editoast/src/redis_utils.rs b/editoast/src/redis_utils.rs index 320347d1f19..36f8738e152 100644 --- a/editoast/src/redis_utils.rs +++ b/editoast/src/redis_utils.rs @@ -1,10 +1,12 @@ -use crate::client::RedisConfig; -use crate::error::Result; +use std::fmt::Debug; + use futures::future; use futures::FutureExt; -use redis::aio::{ConnectionLike, ConnectionManager}; +use redis::aio::ConnectionLike; +use redis::aio::ConnectionManager; use redis::cluster::ClusterClient; use redis::cluster_async::ClusterConnection; +use redis::cmd; use redis::AsyncCommands; use redis::Client; use redis::ErrorKind; @@ -14,7 +16,9 @@ use redis::RedisResult; use redis::ToRedisArgs; use serde::de::DeserializeOwned; use serde::Serialize; -use std::fmt::Debug; + +use crate::client::RedisConfig; +use crate::error::Result; pub enum RedisConnection { Cluster(ClusterConnection), @@ -194,4 +198,10 @@ impl RedisClient { RedisClient::NoCache => Ok(RedisConnection::NoCache), } } + + pub async fn ping_redis(&self) -> RedisResult<()> { + let mut conn = self.get_connection().await?; + cmd("PING").query_async::<_, ()>(&mut conn).await?; + Ok(()) + } } diff --git a/editoast/src/views/mod.rs b/editoast/src/views/mod.rs index e03c633476d..bf507c41e69 100644 --- a/editoast/src/views/mod.rs +++ b/editoast/src/views/mod.rs @@ -25,16 +25,22 @@ pub mod work_schedules; mod test_app; use std::ops::DerefMut as _; +use std::sync::Arc; +use std::time::Duration; +use futures::TryFutureExt; pub use openapi::OpenApiRoot; use actix_web::get; use actix_web::web::Data; use actix_web::web::Json; -use diesel::sql_query; -use redis::cmd; +use editoast_derive::EditoastError; +use editoast_models::ping_database; +use editoast_models::DbConnectionPoolV2; use serde_derive::Deserialize; use serde_derive::Serialize; +use thiserror::Error; +use tokio::time::timeout; use utoipa::ToSchema; use crate::client::get_app_version; @@ -49,7 +55,6 @@ use crate::infra_cache::operation; use crate::models; use crate::modelsv2; use crate::RedisClient; -use editoast_models::DbConnectionPoolV2; crate::routes! { (health, version, core_version), @@ -95,6 +100,17 @@ editoast_common::schemas! { work_schedules::schemas(), } +#[derive(Debug, Error, EditoastError)] +#[editoast_error(base_id = "app_health")] +pub enum AppHealthError { + #[error("Timeout error")] + Timeout, + #[error(transparent)] + Database(#[from] editoast_models::EditoastModelsError), + #[error(transparent)] + Redis(#[from] redis::RedisError), +} + #[utoipa::path( responses( (status = 200, description = "Check if Editoast is running correctly", body = String) @@ -105,16 +121,27 @@ async fn health( db_pool: Data, redis_client: Data, ) -> Result<&'static str> { - use diesel_async::RunQueryDsl; - sql_query("SELECT 1") - .execute(db_pool.get().await?.deref_mut()) - .await?; - - let mut conn = redis_client.get_connection().await?; - cmd("PING").query_async::<_, ()>(&mut conn).await.unwrap(); + timeout( + Duration::from_millis(500), + check_health(db_pool.into_inner(), redis_client.into_inner()), + ) + .await + .map_err(|_| AppHealthError::Timeout)??; Ok("ok") } +async fn check_health( + db_pool: Arc, + redis_client: Arc, +) -> Result<()> { + let mut db_connection = db_pool.clone().get().await?; + tokio::try_join!( + ping_database(db_connection.deref_mut()).map_err(AppHealthError::Database), + redis_client.ping_redis().map_err(|e| e.into()) + )?; + Ok(()) +} + #[derive(ToSchema, Serialize, Deserialize)] pub struct Version { #[schema(required)] // Options are by default not required, but this one is diff --git a/front/public/locales/en/errors.json b/front/public/locales/en/errors.json index 48d05934e23..664d19fe104 100644 --- a/front/public/locales/en/errors.json +++ b/front/public/locales/en/errors.json @@ -193,6 +193,11 @@ }, "work_schedule": { "NameAlreadyUsed": "A group of work schedules with '{{name}}' already exists" + }, + "app_health": { + "Timeout": "Service has not responded in time", + "Database": "Database is in error", + "Redis": "Redis is in error" } } } diff --git a/front/public/locales/fr/errors.json b/front/public/locales/fr/errors.json index 7c33123b3b6..64c5566f5d6 100644 --- a/front/public/locales/fr/errors.json +++ b/front/public/locales/fr/errors.json @@ -195,6 +195,11 @@ }, "work_schedule": { "NameAlreadyUsed": "Un groupe de planches travaux avec le nom '{{name}}' existe déjà" + }, + "app_health": { + "Timeout": "Le serveur n'a pas répondu à temps", + "Database": "Erreur de base de données", + "Redis": "Erreur de Redis" } } }