relay_knowledge/application/runtime/
worker.rs1use std::{error::Error, fmt};
2
3use crate::{domain::WorkerKind, env::EnvironmentConfig};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct WorkerRuntimeConfig {
8 pub embedding_endpoint: Option<String>,
9 pub ocr_endpoint: Option<String>,
10 pub vision_endpoint: Option<String>,
11 pub extractor_endpoint: Option<String>,
12 pub max_in_flight: usize,
13 pub code_index_max_in_flight: usize,
14 pub silent_updates_enabled: bool,
15}
16
17impl WorkerRuntimeConfig {
18 pub const DEFAULT_MAX_IN_FLIGHT: usize = 2;
19 pub const DEFAULT_CODE_INDEX_MAX_IN_FLIGHT: usize = 2;
20 pub const MAX_CODE_INDEX_MAX_IN_FLIGHT: usize = 8;
21
22 pub fn from_environment(
24 environment: &EnvironmentConfig,
25 ) -> Result<Self, WorkerRuntimeConfigError> {
26 Ok(Self {
27 embedding_endpoint: validate_worker_endpoint(
28 environment.workers.embedding_endpoint.clone(),
29 )?,
30 ocr_endpoint: validate_worker_endpoint(environment.workers.ocr_endpoint.clone())?,
31 vision_endpoint: validate_worker_endpoint(environment.workers.vision_endpoint.clone())?,
32 extractor_endpoint: validate_worker_endpoint(
33 environment.workers.extractor_endpoint.clone(),
34 )?,
35 max_in_flight: environment
36 .workers
37 .max_in_flight
38 .unwrap_or(Self::DEFAULT_MAX_IN_FLIGHT),
39 code_index_max_in_flight: environment
40 .workers
41 .code_index_max_in_flight
42 .unwrap_or(Self::DEFAULT_CODE_INDEX_MAX_IN_FLIGHT)
43 .min(Self::MAX_CODE_INDEX_MAX_IN_FLIGHT),
44 silent_updates_enabled: environment.workers.silent_updates_enabled.unwrap_or(false),
45 })
46 }
47
48 pub fn endpoint_for(&self, kind: WorkerKind) -> Option<&str> {
50 match kind {
51 WorkerKind::Embedding => self.embedding_endpoint.as_deref(),
52 WorkerKind::Ocr => self.ocr_endpoint.as_deref(),
53 WorkerKind::Vision => self.vision_endpoint.as_deref(),
54 WorkerKind::Extractor => self.extractor_endpoint.as_deref(),
55 }
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
61pub enum WorkerRuntimeConfigError {
62 InvalidEndpoint(String),
63}
64
65impl fmt::Display for WorkerRuntimeConfigError {
66 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
67 match self {
68 Self::InvalidEndpoint(value) => write!(
69 formatter,
70 "worker endpoint '{value}' must use http:// and include a host"
71 ),
72 }
73 }
74}
75
76impl Error for WorkerRuntimeConfigError {}
77
78fn validate_worker_endpoint(
79 value: Option<String>,
80) -> Result<Option<String>, WorkerRuntimeConfigError> {
81 value
82 .map(|endpoint| {
83 let trimmed = endpoint.trim();
84 if is_valid_worker_http_endpoint(trimmed) {
85 Ok(trimmed.to_owned())
86 } else {
87 Err(WorkerRuntimeConfigError::InvalidEndpoint(endpoint))
88 }
89 })
90 .transpose()
91}
92
93fn is_valid_worker_http_endpoint(value: &str) -> bool {
94 let Some(remainder) = value.strip_prefix("http://") else {
95 return false;
96 };
97 let authority = remainder
98 .split_once('/')
99 .map_or(remainder, |(authority, _)| authority);
100 if authority.is_empty() || authority.contains(char::is_whitespace) {
101 return false;
102 }
103 if let Some((host, port)) = authority.rsplit_once(':') {
104 return !host.is_empty() && port.parse::<u16>().is_ok_and(|port| port > 0);
105 }
106
107 !authority.is_empty()
108}
109
110#[cfg(test)]
111#[path = "worker_tests.rs"]
112mod tests;