perf(prop-grid-rs): claim N forecast hours concurrently
Replaces the serial claim→process loop with a JoinSet of N worker tasks, each running the same claim→fetch→decode→score→complete flow against grid_tasks. FOR UPDATE SKIP LOCKED already prevents contention between workers so no additional coordination is needed. talos5 is 4c/4t (i5-7260U); PROP_GRID_RS_PARALLELISM=4 turns the full f01..f18 chain from ~18 min serial into roughly ~5 min. Per-task peak memory after the streamed-bands refactor is ~250 Mi, so 4 concurrent is ~1 Gi — well under the 2 Gi container limit. Postgres pool sized to parallelism + 2 so NOTIFY and an opportunistic claim retry share the pool without blocking a worker transaction.
This commit is contained in:
parent
7b1de3f995
commit
022e6928bc
2 changed files with 61 additions and 14 deletions
|
|
@ -61,8 +61,17 @@ spec:
|
||||||
value: "/data/scores"
|
value: "/data/scores"
|
||||||
- name: RUST_LOG
|
- name: RUST_LOG
|
||||||
value: "info"
|
value: "info"
|
||||||
- name: PROP_GRID_RS_PG_CONNS
|
# talos5 is 4c/4t (i5-7260U). Claiming 4 forecast hours in
|
||||||
|
# parallel drops full-chain wall time from ~18 min serial to
|
||||||
|
# ~5 min. Each concurrent task peaks around 250 Mi after the
|
||||||
|
# streamed-bands refactor; 4×250 ≈ 1 Gi is comfortably under
|
||||||
|
# the 2 Gi memory limit.
|
||||||
|
- name: PROP_GRID_RS_PARALLELISM
|
||||||
value: "4"
|
value: "4"
|
||||||
|
# Pool size = parallelism + 2 (NOTIFY + retry slack). Kept in
|
||||||
|
# sync with PROP_GRID_RS_PARALLELISM when either changes.
|
||||||
|
- name: PROP_GRID_RS_PG_CONNS
|
||||||
|
value: "6"
|
||||||
envFrom:
|
envFrom:
|
||||||
- secretRef:
|
- secretRef:
|
||||||
name: prop-secrets
|
name: prop-secrets
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,13 @@
|
||||||
//!
|
//!
|
||||||
//! Flow:
|
//! Flow:
|
||||||
//! 1. Connect to Postgres (DATABASE_URL env).
|
//! 1. Connect to Postgres (DATABASE_URL env).
|
||||||
//! 2. Loop: claim one `grid_tasks` row (fh>0, status='queued'),
|
//! 2. Spawn N concurrent worker tasks (`PROP_GRID_RS_PARALLELISM`, default
|
||||||
//! run the fetch → decode → score → write-scores-file chain, mark done +
|
//! = num_cpus). Each worker independently claims one `grid_tasks` row
|
||||||
//! `NOTIFY propagation_ready '<valid_time iso>'`. On error mark
|
//! (fh>0, status='queued') via `FOR UPDATE SKIP LOCKED`, runs the
|
||||||
|
//! fetch → decode → score → write-scores-file chain, and marks done +
|
||||||
|
//! `NOTIFY propagation_ready '<valid_time iso>'`. On error marks
|
||||||
//! failed; hourly cron reseeds.
|
//! failed; hourly cron reseeds.
|
||||||
//! 3. Graceful shutdown on SIGTERM: requeue current row and exit.
|
//! 3. Graceful shutdown on SIGTERM: workers exit after their current task.
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -14,6 +16,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use prop_grid_rs::{db, fetcher::HrrrClient, pipeline};
|
use prop_grid_rs::{db, fetcher::HrrrClient, pipeline};
|
||||||
|
use tokio::task::JoinSet;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
|
@ -33,10 +36,18 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
.unwrap_or_else(|_| PathBuf::from("/data/scores"));
|
.unwrap_or_else(|_| PathBuf::from("/data/scores"));
|
||||||
let hrrr_base = std::env::var("HRRR_BASE_URL")
|
let hrrr_base = std::env::var("HRRR_BASE_URL")
|
||||||
.unwrap_or_else(|_| prop_grid_rs::fetcher::HRRR_BASE_DEFAULT.to_string());
|
.unwrap_or_else(|_| prop_grid_rs::fetcher::HRRR_BASE_DEFAULT.to_string());
|
||||||
|
let parallelism: usize = std::env::var("PROP_GRID_RS_PARALLELISM")
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or_else(num_cpus::get)
|
||||||
|
.max(1);
|
||||||
|
// +2 slots over parallelism: each worker holds one connection during
|
||||||
|
// claim/complete transactions; the spare lets NOTIFY and an opportunistic
|
||||||
|
// retry share the pool without blocking.
|
||||||
let max_conn: u32 = std::env::var("PROP_GRID_RS_PG_CONNS")
|
let max_conn: u32 = std::env::var("PROP_GRID_RS_PG_CONNS")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|v| v.parse().ok())
|
.and_then(|v| v.parse().ok())
|
||||||
.unwrap_or(4);
|
.unwrap_or((parallelism as u32) + 2);
|
||||||
|
|
||||||
let pool = db::connect(&database_url, max_conn).await?;
|
let pool = db::connect(&database_url, max_conn).await?;
|
||||||
let client = Arc::new(HrrrClient::new(hrrr_base)?);
|
let client = Arc::new(HrrrClient::new(hrrr_base)?);
|
||||||
|
|
@ -44,8 +55,37 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let shutdown = Arc::new(AtomicBool::new(false));
|
let shutdown = Arc::new(AtomicBool::new(false));
|
||||||
spawn_signal_handler(shutdown.clone());
|
spawn_signal_handler(shutdown.clone());
|
||||||
|
|
||||||
info!(scores_dir = %scores_dir.display(), "prop-grid-rs started");
|
info!(
|
||||||
|
scores_dir = %scores_dir.display(),
|
||||||
|
parallelism,
|
||||||
|
pg_conns = max_conn,
|
||||||
|
"prop-grid-rs started"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut set: JoinSet<()> = JoinSet::new();
|
||||||
|
for worker_id in 0..parallelism {
|
||||||
|
let pool = pool.clone();
|
||||||
|
let client = client.clone();
|
||||||
|
let scores_dir = scores_dir.clone();
|
||||||
|
let shutdown = shutdown.clone();
|
||||||
|
set.spawn(async move {
|
||||||
|
worker_loop(worker_id, pool, client, scores_dir, shutdown).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
while set.join_next().await.is_some() {}
|
||||||
|
|
||||||
|
info!("prop-grid-rs shutting down");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn worker_loop(
|
||||||
|
worker_id: usize,
|
||||||
|
pool: sqlx::PgPool,
|
||||||
|
client: Arc<HrrrClient>,
|
||||||
|
scores_dir: PathBuf,
|
||||||
|
shutdown: Arc<AtomicBool>,
|
||||||
|
) {
|
||||||
while !shutdown.load(Ordering::Relaxed) {
|
while !shutdown.load(Ordering::Relaxed) {
|
||||||
match db::claim_next(&pool).await {
|
match db::claim_next(&pool).await {
|
||||||
Ok(Some(task)) => {
|
Ok(Some(task)) => {
|
||||||
|
|
@ -62,6 +102,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
{
|
{
|
||||||
Ok(stats) => {
|
Ok(stats) => {
|
||||||
info!(
|
info!(
|
||||||
|
worker_id,
|
||||||
task_id = %task.id,
|
task_id = %task.id,
|
||||||
run_time = %task.run_time,
|
run_time = %task.run_time,
|
||||||
forecast_hour = task.forecast_hour,
|
forecast_hour = task.forecast_hour,
|
||||||
|
|
@ -70,13 +111,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
"chain step done"
|
"chain step done"
|
||||||
);
|
);
|
||||||
if let Err(e) = db::complete(&pool, &task).await {
|
if let Err(e) = db::complete(&pool, &task).await {
|
||||||
error!(%task_id, error = %e, "failed to mark task done");
|
error!(worker_id, %task_id, error = %e, "failed to mark task done");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!(%task_id, error = %e, "chain step failed");
|
warn!(worker_id, %task_id, error = %e, "chain step failed");
|
||||||
if let Err(dbe) = db::fail(&pool, task_id, &e.to_string()).await {
|
if let Err(dbe) = db::fail(&pool, task_id, &e.to_string()).await {
|
||||||
error!(%task_id, error = %dbe, "failed to mark task failed");
|
error!(worker_id, %task_id, error = %dbe, "failed to mark task failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -85,14 +126,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
tokio::time::sleep(IDLE_SLEEP).await;
|
tokio::time::sleep(IDLE_SLEEP).await;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(error = %e, "claim_next failed; backing off");
|
error!(worker_id, error = %e, "claim_next failed; backing off");
|
||||||
tokio::time::sleep(IDLE_SLEEP).await;
|
tokio::time::sleep(IDLE_SLEEP).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("prop-grid-rs shutting down");
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue