feat: enhance concurrency settings for rendering and thumbnail generation
- Introduce dynamic loading of concurrent render limits from the database for both page rendering and thumbnail generation. - Update API to utilize the loaded concurrency settings, defaulting to 8 for page renders and 4 for thumbnails. - Modify front-end settings page to reflect changes in concurrency limits and provide user guidance on their impact. - Ensure that changes to limits require a server restart to take effect, with clear messaging in the UI.
This commit is contained in:
@@ -32,6 +32,8 @@ use stripstream_core::config::ApiConfig;
|
|||||||
use sqlx::postgres::PgPoolOptions;
|
use sqlx::postgres::PgPoolOptions;
|
||||||
use tokio::sync::{Mutex, Semaphore};
|
use tokio::sync::{Mutex, Semaphore};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
use sqlx::{Pool, Postgres, Row};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct AppState {
|
struct AppState {
|
||||||
@@ -66,6 +68,25 @@ impl Metrics {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn load_concurrent_renders(pool: &Pool<Postgres>) -> usize {
|
||||||
|
let default_concurrency = 8;
|
||||||
|
let row = sqlx::query(r#"SELECT value FROM app_settings WHERE key = 'limits'"#)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match row {
|
||||||
|
Ok(Some(row)) => {
|
||||||
|
let value: Value = row.get("value");
|
||||||
|
value
|
||||||
|
.get("concurrent_renders")
|
||||||
|
.and_then(|v: &Value| v.as_u64())
|
||||||
|
.map(|v| v as usize)
|
||||||
|
.unwrap_or(default_concurrency)
|
||||||
|
}
|
||||||
|
_ => default_concurrency,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
@@ -80,13 +101,17 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.connect(&config.database_url)
|
.connect(&config.database_url)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// Load concurrent_renders from settings, default to 8
|
||||||
|
let concurrent_renders = load_concurrent_renders(&pool).await;
|
||||||
|
info!("Using concurrent_renders limit: {}", concurrent_renders);
|
||||||
|
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
pool,
|
pool,
|
||||||
bootstrap_token: Arc::from(config.api_bootstrap_token),
|
bootstrap_token: Arc::from(config.api_bootstrap_token),
|
||||||
meili_url: Arc::from(config.meili_url),
|
meili_url: Arc::from(config.meili_url),
|
||||||
meili_master_key: Arc::from(config.meili_master_key),
|
meili_master_key: Arc::from(config.meili_master_key),
|
||||||
page_cache: Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(512).expect("non-zero")))),
|
page_cache: Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(512).expect("non-zero")))),
|
||||||
page_render_limit: Arc::new(Semaphore::new(8)),
|
page_render_limit: Arc::new(Semaphore::new(concurrent_renders)),
|
||||||
metrics: Arc::new(Metrics::new()),
|
metrics: Arc::new(Metrics::new()),
|
||||||
read_rate_limit: Arc::new(Mutex::new(ReadRateLimit {
|
read_rate_limit: Arc::new(Mutex::new(ReadRateLimit {
|
||||||
window_started_at: Instant::now(),
|
window_started_at: Instant::now(),
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::sync::atomic::{AtomicI32, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use axum::{
|
use axum::{
|
||||||
@@ -6,6 +8,7 @@ use axum::{
|
|||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
Json,
|
Json,
|
||||||
};
|
};
|
||||||
|
use futures::stream::{self, StreamExt};
|
||||||
use image::GenericImageView;
|
use image::GenericImageView;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::Row;
|
use sqlx::Row;
|
||||||
@@ -24,6 +27,25 @@ struct ThumbnailConfig {
|
|||||||
directory: String,
|
directory: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn load_thumbnail_concurrency(pool: &sqlx::PgPool) -> usize {
|
||||||
|
let default_concurrency = 4;
|
||||||
|
let row = sqlx::query(r#"SELECT value FROM app_settings WHERE key = 'limits'"#)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match row {
|
||||||
|
Ok(Some(row)) => {
|
||||||
|
let value: serde_json::Value = row.get("value");
|
||||||
|
value
|
||||||
|
.get("concurrent_renders")
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.map(|v| v as usize)
|
||||||
|
.unwrap_or(default_concurrency)
|
||||||
|
}
|
||||||
|
_ => default_concurrency,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn load_thumbnail_config(pool: &sqlx::PgPool) -> ThumbnailConfig {
|
async fn load_thumbnail_config(pool: &sqlx::PgPool) -> ThumbnailConfig {
|
||||||
let fallback = ThumbnailConfig {
|
let fallback = ThumbnailConfig {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -156,38 +178,56 @@ async fn run_checkup(state: AppState, job_id: Uuid) {
|
|||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
for (i, &book_id) in book_ids.iter().enumerate() {
|
let concurrency = load_thumbnail_concurrency(pool).await;
|
||||||
match pages::render_book_page_1(&state, book_id, config.width, config.quality).await {
|
let processed_count = Arc::new(AtomicI32::new(0));
|
||||||
Ok(page_bytes) => {
|
let pool_clone = pool.clone();
|
||||||
match generate_thumbnail(&page_bytes, &config) {
|
let job_id_clone = job_id;
|
||||||
Ok(thumb_bytes) => {
|
let config_clone = config.clone();
|
||||||
if let Ok(path) = save_thumbnail(book_id, &thumb_bytes, &config) {
|
let state_clone = state.clone();
|
||||||
if sqlx::query("UPDATE books SET thumbnail_path = $1 WHERE id = $2")
|
|
||||||
.bind(&path)
|
stream::iter(book_ids)
|
||||||
.bind(book_id)
|
.for_each_concurrent(concurrency, |book_id| {
|
||||||
.execute(pool)
|
let processed_count = processed_count.clone();
|
||||||
.await
|
let pool = pool_clone.clone();
|
||||||
.is_ok()
|
let job_id = job_id_clone;
|
||||||
{
|
let config = config_clone.clone();
|
||||||
let processed = (i + 1) as i32;
|
let state = state_clone.clone();
|
||||||
let percent = ((i + 1) as f64 / total as f64 * 100.0) as i32;
|
let total = total;
|
||||||
let _ = sqlx::query(
|
|
||||||
"UPDATE index_jobs SET processed_files = $2, progress_percent = $3 WHERE id = $1",
|
async move {
|
||||||
)
|
match pages::render_book_page_1(&state, book_id, config.width, config.quality).await {
|
||||||
.bind(job_id)
|
Ok(page_bytes) => {
|
||||||
.bind(processed)
|
match generate_thumbnail(&page_bytes, &config) {
|
||||||
.bind(percent)
|
Ok(thumb_bytes) => {
|
||||||
.execute(pool)
|
if let Ok(path) = save_thumbnail(book_id, &thumb_bytes, &config) {
|
||||||
.await;
|
if sqlx::query("UPDATE books SET thumbnail_path = $1 WHERE id = $2")
|
||||||
|
.bind(&path)
|
||||||
|
.bind(book_id)
|
||||||
|
.execute(&pool)
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
let processed = processed_count.fetch_add(1, Ordering::Relaxed) + 1;
|
||||||
|
let percent = (processed as f64 / total as f64 * 100.0) as i32;
|
||||||
|
let _ = sqlx::query(
|
||||||
|
"UPDATE index_jobs SET processed_files = $2, progress_percent = $3 WHERE id = $1",
|
||||||
|
)
|
||||||
|
.bind(job_id)
|
||||||
|
.bind(processed)
|
||||||
|
.bind(percent)
|
||||||
|
.execute(&pool)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Err(e) => warn!("thumbnail generate failed for book {}: {:?}", book_id, e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => warn!("thumbnail generate failed for book {}: {:?}", book_id, e),
|
Err(e) => warn!("render page 1 failed for book {}: {:?}", book_id, e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => warn!("render page 1 failed for book {}: {:?}", book_id, e),
|
})
|
||||||
}
|
.await;
|
||||||
}
|
|
||||||
|
|
||||||
let _ = sqlx::query(
|
let _ = sqlx::query(
|
||||||
"UPDATE index_jobs SET status = 'success', finished_at = NOW(), progress_percent = 100, current_file = NULL WHERE id = $1",
|
"UPDATE index_jobs SET status = 'success', finished_at = NOW(), progress_percent = 100, current_file = NULL WHERE id = $1",
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ export default function SettingsPage({ initialSettings, initialCacheStats, initi
|
|||||||
<Icon name="performance" size="md" />
|
<Icon name="performance" size="md" />
|
||||||
Performance Limits
|
Performance Limits
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>Configure API performance and rate limiting</CardDescription>
|
<CardDescription>Configure API performance, rate limiting, and thumbnail generation concurrency</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -266,6 +266,9 @@ export default function SettingsPage({ initialSettings, initialCacheStats, initi
|
|||||||
}}
|
}}
|
||||||
onBlur={() => handleUpdateSetting("limits", settings.limits)}
|
onBlur={() => handleUpdateSetting("limits", settings.limits)}
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Maximum number of page renders and thumbnail generations running in parallel
|
||||||
|
</p>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField className="flex-1">
|
<FormField className="flex-1">
|
||||||
<label className="text-sm font-medium text-muted-foreground mb-1 block">Timeout (seconds)</label>
|
<label className="text-sm font-medium text-muted-foreground mb-1 block">Timeout (seconds)</label>
|
||||||
@@ -299,7 +302,7 @@ export default function SettingsPage({ initialSettings, initialCacheStats, initi
|
|||||||
</FormField>
|
</FormField>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Note: Changes to limits require a server restart to take effect.
|
Note: Changes to limits require a server restart to take effect. The "Concurrent Renders" setting controls both page rendering and thumbnail generation parallelism.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -424,7 +427,7 @@ export default function SettingsPage({ initialSettings, initialCacheStats, initi
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Note: Thumbnail settings are used during indexing. Existing thumbnails will not be regenerated automatically.
|
Note: Thumbnail settings are used during indexing. Existing thumbnails will not be regenerated automatically. The concurrency for thumbnail generation is controlled by the "Concurrent Renders" setting in Performance Limits above.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user