feat: workerize external generation

This commit is contained in:
2026-06-05 17:29:08 +08:00
parent 5150925947
commit 8d54ea3374
60 changed files with 5285 additions and 700 deletions

View File

@@ -21,6 +21,11 @@ pub struct AppConfig {
pub bind_port: u16,
pub listen_backlog: i32,
pub worker_threads: Option<usize>,
pub process_role: ProcessRole,
pub external_generation_worker_id: String,
pub external_generation_worker_concurrency: usize,
pub external_generation_worker_poll_interval: Duration,
pub external_generation_worker_lease: Duration,
pub max_concurrent_requests: Option<usize>,
pub gallery_max_concurrent_requests: Option<usize>,
pub detail_max_concurrent_requests: Option<usize>,
@@ -159,6 +164,31 @@ pub struct AppConfig {
pub slow_request_threshold_ms: u64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ProcessRole {
Api,
ExternalGenerationWorker,
All,
}
impl ProcessRole {
pub fn as_str(self) -> &'static str {
match self {
Self::Api => "api",
Self::ExternalGenerationWorker => "external-generation-worker",
Self::All => "all",
}
}
pub fn runs_http(self) -> bool {
matches!(self, Self::Api | Self::All)
}
pub fn runs_external_generation_worker(self) -> bool {
matches!(self, Self::ExternalGenerationWorker | Self::All)
}
}
impl Default for AppConfig {
fn default() -> Self {
Self {
@@ -166,6 +196,11 @@ impl Default for AppConfig {
bind_port: 3000,
listen_backlog: 1024,
worker_threads: None,
process_role: ProcessRole::Api,
external_generation_worker_id: default_external_generation_worker_id(),
external_generation_worker_concurrency: 2,
external_generation_worker_poll_interval: Duration::from_millis(2_000),
external_generation_worker_lease: Duration::from_secs(3_600),
max_concurrent_requests: None,
gallery_max_concurrent_requests: None,
detail_max_concurrent_requests: None,
@@ -347,6 +382,30 @@ impl AppConfig {
if let Some(worker_threads) = read_first_usize_env(&["GENARRATIVE_API_WORKER_THREADS"]) {
config.worker_threads = Some(worker_threads);
}
if let Some(process_role) = read_first_process_role_env(&["GENARRATIVE_PROCESS_ROLE"]) {
config.process_role = process_role;
}
if let Some(worker_id) =
read_first_non_empty_env(&["GENARRATIVE_EXTERNAL_GENERATION_WORKER_ID"])
{
config.external_generation_worker_id = worker_id;
}
if let Some(concurrency) =
read_first_usize_env(&["GENARRATIVE_EXTERNAL_GENERATION_WORKER_CONCURRENCY"])
{
config.external_generation_worker_concurrency = concurrency.max(1);
}
if let Some(poll_interval_ms) = read_first_positive_u64_env(&[
"GENARRATIVE_EXTERNAL_GENERATION_WORKER_POLL_INTERVAL_MS",
]) {
config.external_generation_worker_poll_interval =
Duration::from_millis(poll_interval_ms);
}
if let Some(lease_seconds) = read_first_duration_seconds_env(&[
"GENARRATIVE_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS",
]) {
config.external_generation_worker_lease = Duration::from_secs(lease_seconds.max(1));
}
if let Some(max_concurrent_requests) =
read_first_usize_env(&["GENARRATIVE_API_MAX_CONCURRENT_REQUESTS"])
{
@@ -979,6 +1038,14 @@ fn read_first_llm_provider_env(keys: &[&str]) -> Option<LlmProvider> {
})
}
fn read_first_process_role_env(keys: &[&str]) -> Option<ProcessRole> {
keys.iter().find_map(|key| {
env::var(key)
.ok()
.and_then(|value| parse_process_role(&value))
})
}
fn read_first_positive_u32_env(keys: &[&str]) -> Option<u32> {
keys.iter().find_map(|key| {
env::var(key)
@@ -1026,6 +1093,36 @@ fn read_first_u8_env(keys: &[&str]) -> Option<u8> {
.find_map(|key| env::var(key).ok().and_then(|value| parse_u8(&value)))
}
fn default_external_generation_worker_id() -> String {
let host = env::var("HOSTNAME")
.or_else(|_| env::var("COMPUTERNAME"))
.unwrap_or_else(|_| "local".to_string());
format!("{}-{}", host.trim(), std::process::id())
}
fn parse_process_role(value: &str) -> Option<ProcessRole> {
match trim_quoted_env_value(value).to_ascii_lowercase().as_str() {
"api" => Some(ProcessRole::Api),
"external-generation-worker" | "external_generation_worker" | "worker" => {
Some(ProcessRole::ExternalGenerationWorker)
}
"all" => Some(ProcessRole::All),
_ => None,
}
}
fn trim_quoted_env_value(raw: &str) -> &str {
let raw = raw.trim();
raw.strip_prefix('"')
.and_then(|value| value.strip_suffix('"'))
.or_else(|| {
raw.strip_prefix('\'')
.and_then(|value| value.strip_suffix('\''))
})
.unwrap_or(raw)
.trim()
}
fn read_first_positive_u16_env(keys: &[&str]) -> Option<u16> {
keys.iter().find_map(|key| {
env::var(key)
@@ -1146,7 +1243,8 @@ fn parse_positive_u16(raw: &str) -> Option<u16> {
#[cfg(test)]
mod tests {
use super::{
AppConfig, DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS, LlmProvider, parse_bool,
AppConfig, DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS, LlmProvider, ProcessRole,
parse_bool, parse_process_role,
};
use std::sync::{Mutex, OnceLock};
@@ -1188,6 +1286,32 @@ mod tests {
assert_eq!(parse_bool("'off'"), Some(false));
}
#[test]
fn process_role_controls_http_and_external_generation_worker_roles() {
assert_eq!(parse_process_role("api"), Some(ProcessRole::Api));
assert_eq!(
parse_process_role("\"external-generation-worker\""),
Some(ProcessRole::ExternalGenerationWorker)
);
assert_eq!(
parse_process_role("'external_generation_worker'"),
Some(ProcessRole::ExternalGenerationWorker)
);
assert_eq!(
parse_process_role("worker"),
Some(ProcessRole::ExternalGenerationWorker)
);
assert_eq!(parse_process_role("all"), Some(ProcessRole::All));
assert_eq!(parse_process_role("unknown"), None);
assert!(ProcessRole::Api.runs_http());
assert!(!ProcessRole::Api.runs_external_generation_worker());
assert!(!ProcessRole::ExternalGenerationWorker.runs_http());
assert!(ProcessRole::ExternalGenerationWorker.runs_external_generation_worker());
assert!(ProcessRole::All.runs_http());
assert!(ProcessRole::All.runs_external_generation_worker());
}
#[test]
fn from_env_reads_sms_enabled_when_shell_value_keeps_quotes() {
let _guard = ENV_LOCK