-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge #395: Implement health check for the Index container
0e8b32b ci: [#254] add container healthcheck for index service (Jose Celano) Pull request description: [After discussing how to implement a proper container HEALTHCHECK for the Index ](#254) this final implementation: - Doesn't use `curl`, `wget` or other system dependency to [avoid maintaining that dependency and security attacks](https://blog.sixeyed.com/docker-healthchecks-why-not-to-use-curl-or-iwr/). It uses a new binary that you can use like this: `cargo run --bin health_check http://localhost:PORT/health_check`. - Adds an API to the tracker statistics importer so that other processes can communicate with the cronjob. - Exposes a `/health_check` endpoint in both APIs: the Index API and the Importer API (console cronjob). - The endpoint for the Index core API is public but the one for the cronjob it runs only on the localhost. You can run the `health_check` from inside the container with the following commands: For the API: ```console / # /usr/bin/health_check http://localhost:3001/health_check Health check ... STATUS: 200 OK ``` For the Importer: ```console / # /usr/bin/health_check http://localhost:3002/health_check Health check ... STATUS: 200 OK ``` ### NOTES - The Index application has two services: the rest API and the tracker statistic importer. The healthcheck should check both. The API service should not know the "importer". - For the time being, we do not check the services properly. We only ensure that they are running. In the future, we can add more checks to the `health_check` handlers like checking database connection, SMTP server, connection to the tracker, etcetera. However, I don't know if we should check those things here. This healthcheck is only to ensure the container's processes are up and running. Maybe it's not the right place to check its dependencies. Those dependencies can fail at runtime. Maybe a better approach would be to monitor each service independently. - The new `/health_check` endpoint could be used for monitoring. - Right now, It's only used to wait for the container to be `healthy` after `docker compose up`. ACKs for top commit: josecelano: ACK 0e8b32b Tree-SHA512: cde75ae0f9901896e55db660a782b3b2d6030e5c09a9384dfadb5f8dee0ed650db7c842ce1e152b400b2ae8cc87047840d20e00facd700538d320c5bfa217d6c
- Loading branch information
Showing
16 changed files
with
220 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -48,3 +48,4 @@ max_torrent_page_size = 30 | |
|
||
[tracker_statistics_importer] | ||
torrent_info_update_interval = 3600 | ||
port = 3002 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -48,3 +48,4 @@ max_torrent_page_size = 30 | |
|
||
[tracker_statistics_importer] | ||
torrent_info_update_interval = 3600 | ||
port = 3002 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,3 +44,4 @@ max_torrent_page_size = 30 | |
|
||
[tracker_statistics_importer] | ||
torrent_info_update_interval = 3600 | ||
port = 3002 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
//! Minimal `curl` or `wget` to be used for container health checks. | ||
//! | ||
//! It's convenient to avoid using third-party libraries because: | ||
//! | ||
//! - They are harder to maintain. | ||
//! - They introduce new attack vectors. | ||
use std::{env, process}; | ||
|
||
#[tokio::main] | ||
async fn main() { | ||
let args: Vec<String> = env::args().collect(); | ||
if args.len() != 2 { | ||
eprintln!("Usage: cargo run --bin health_check <HEALTH_URL>"); | ||
eprintln!("Example: cargo run --bin health_check http://localhost:3002/health_check"); | ||
std::process::exit(1); | ||
} | ||
|
||
println!("Health check ..."); | ||
|
||
let url = &args[1].clone(); | ||
|
||
match reqwest::get(url).await { | ||
Ok(response) => { | ||
if response.status().is_success() { | ||
println!("STATUS: {}", response.status()); | ||
process::exit(0); | ||
} else { | ||
println!("Non-success status received."); | ||
process::exit(1); | ||
} | ||
} | ||
Err(err) => { | ||
println!("ERROR: {err}"); | ||
process::exit(1); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
pub mod commands; | ||
pub(crate) mod tracker_statistics_importer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
//! Cronjob to import tracker torrent data and updating seeders and leechers | ||
//! info. | ||
//! | ||
//! It has two services: | ||
//! | ||
//! - The importer which is the cronjob executed at regular intervals. | ||
//! - The importer API. | ||
//! | ||
//! The cronjob sends a heartbeat signal to the API each time it is executed. | ||
//! The last heartbeat signal time is used to determine whether the cronjob was | ||
//! executed successfully or not. The API has a `health_check` endpoint which is | ||
//! used when the application is running in containers. | ||
use std::sync::{Arc, Mutex}; | ||
|
||
use axum::extract::State; | ||
use axum::routing::{get, post}; | ||
use axum::{Json, Router}; | ||
use chrono::{DateTime, Utc}; | ||
use log::{error, info}; | ||
use serde_json::{json, Value}; | ||
use tokio::task::JoinHandle; | ||
|
||
use crate::tracker::statistics_importer::StatisticsImporter; | ||
|
||
const IMPORTER_API_IP: &str = "127.0.0.1"; | ||
|
||
#[derive(Clone)] | ||
struct ImporterState { | ||
/// Shared variable to store the timestamp of the last heartbeat sent | ||
/// by the cronjob. | ||
pub last_heartbeat: Arc<Mutex<DateTime<Utc>>>, | ||
/// Interval between importation executions | ||
pub torrent_info_update_interval: u64, | ||
} | ||
|
||
pub fn start( | ||
importer_port: u16, | ||
torrent_info_update_interval: u64, | ||
tracker_statistics_importer: &Arc<StatisticsImporter>, | ||
) -> JoinHandle<()> { | ||
let weak_tracker_statistics_importer = Arc::downgrade(tracker_statistics_importer); | ||
|
||
tokio::spawn(async move { | ||
info!("Tracker statistics importer launcher started"); | ||
|
||
// Start the Importer API | ||
|
||
let _importer_api_handle = tokio::spawn(async move { | ||
let import_state = Arc::new(ImporterState { | ||
last_heartbeat: Arc::new(Mutex::new(Utc::now())), | ||
torrent_info_update_interval, | ||
}); | ||
|
||
let app = Router::new() | ||
.route("/", get(|| async { Json(json!({})) })) | ||
.route("/health_check", get(health_check_handler)) | ||
.with_state(import_state.clone()) | ||
.route("/heartbeat", post(heartbeat_handler)) | ||
.with_state(import_state); | ||
|
||
let addr = format!("{IMPORTER_API_IP}:{importer_port}"); | ||
|
||
info!("Tracker statistics importer API server listening on http://{}", addr); | ||
|
||
axum::Server::bind(&addr.parse().unwrap()) | ||
.serve(app.into_make_service()) | ||
.await | ||
.unwrap(); | ||
}); | ||
|
||
// Start the Importer cronjob | ||
|
||
info!("Tracker statistics importer cronjob starting ..."); | ||
|
||
let interval = std::time::Duration::from_secs(torrent_info_update_interval); | ||
let mut interval = tokio::time::interval(interval); | ||
|
||
interval.tick().await; // first tick is immediate... | ||
|
||
loop { | ||
interval.tick().await; | ||
|
||
info!("Running tracker statistics importer ..."); | ||
|
||
if let Err(e) = send_heartbeat(importer_port).await { | ||
error!("Failed to send heartbeat from importer cronjob: {}", e); | ||
} | ||
|
||
if let Some(tracker) = weak_tracker_statistics_importer.upgrade() { | ||
drop(tracker.import_all_torrents_statistics().await); | ||
} else { | ||
break; | ||
} | ||
} | ||
}) | ||
} | ||
|
||
/// Endpoint for container health check. | ||
async fn health_check_handler(State(state): State<Arc<ImporterState>>) -> Json<Value> { | ||
let margin_in_seconds = 10; | ||
let now = Utc::now(); | ||
let last_heartbeat = state.last_heartbeat.lock().unwrap(); | ||
|
||
if now.signed_duration_since(*last_heartbeat).num_seconds() | ||
<= (state.torrent_info_update_interval + margin_in_seconds).try_into().unwrap() | ||
{ | ||
Json(json!({ "status": "Ok" })) | ||
} else { | ||
Json(json!({ "status": "Error" })) | ||
} | ||
} | ||
|
||
/// The tracker statistics importer cronjob sends a heartbeat on each execution | ||
/// to inform that it's alive. This endpoint handles receiving that signal. | ||
async fn heartbeat_handler(State(state): State<Arc<ImporterState>>) -> Json<Value> { | ||
let now = Utc::now(); | ||
let mut last_heartbeat = state.last_heartbeat.lock().unwrap(); | ||
*last_heartbeat = now; | ||
Json(json!({ "status": "Heartbeat received" })) | ||
} | ||
|
||
/// Send a heartbeat from the importer cronjob to the importer API. | ||
async fn send_heartbeat(importer_port: u16) -> Result<(), reqwest::Error> { | ||
let client = reqwest::Client::new(); | ||
let url = format!("http://{IMPORTER_API_IP}:{importer_port}/heartbeat"); | ||
|
||
client.post(url).send().await?; | ||
|
||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.