Skip to main content

lenso_service/
endpoint_resolution.rs

1use serde::{Deserialize, Serialize};
2use std::{
3    collections::BTreeMap,
4    error::Error,
5    fmt,
6    sync::{Arc, RwLock},
7};
8
9/// Stable logical input for Service discovery. It intentionally contains no
10/// instance, Workload, endpoint, host, or region identity.
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
12#[serde(transparent)]
13pub struct ServiceReference(String);
14
15impl ServiceReference {
16    #[must_use]
17    pub fn new(value: impl Into<String>) -> Self {
18        Self(value.into())
19    }
20
21    #[must_use]
22    pub fn as_str(&self) -> &str {
23        &self.0
24    }
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "camelCase")]
29pub struct Endpoint {
30    pub address: String,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub operating_region: Option<String>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub failure_domain: Option<String>,
35}
36
37impl Endpoint {
38    #[must_use]
39    pub fn new(address: impl Into<String>) -> Self {
40        Self {
41            address: address.into(),
42            operating_region: None,
43            failure_domain: None,
44        }
45    }
46
47    #[must_use]
48    pub fn in_region(mut self, region: impl Into<String>) -> Self {
49        self.operating_region = Some(region.into());
50        self
51    }
52
53    #[must_use]
54    pub fn in_failure_domain(mut self, failure_domain: impl Into<String>) -> Self {
55        self.failure_domain = Some(failure_domain.into());
56        self
57    }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "camelCase")]
62pub struct EndpointState {
63    pub service: ServiceReference,
64    pub endpoints: Vec<Endpoint>,
65}
66
67impl EndpointState {
68    #[must_use]
69    pub fn new(service: ServiceReference, endpoints: Vec<Endpoint>) -> Self {
70        Self { service, endpoints }
71    }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(rename_all = "snake_case")]
76pub enum EndpointResolutionErrorCode {
77    InvalidEndpointState,
78    SourceUnavailable,
79    NoUsableEndpointState,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "camelCase")]
84pub struct EndpointResolutionError {
85    pub code: EndpointResolutionErrorCode,
86    pub message: String,
87    pub next_action: String,
88}
89
90impl EndpointResolutionError {
91    #[must_use]
92    pub fn source_unavailable(service: &ServiceReference, message: impl Into<String>) -> Self {
93        Self {
94            code: EndpointResolutionErrorCode::SourceUnavailable,
95            message: message.into(),
96            next_action: format!(
97                "Restore the endpoint source for Service Reference `{}`, then retry.",
98                service.as_str()
99            ),
100        }
101    }
102
103    fn no_usable_state(service: &ServiceReference) -> Self {
104        Self {
105            code: EndpointResolutionErrorCode::NoUsableEndpointState,
106            message: format!(
107                "No usable endpoint state exists for Service Reference `{}`.",
108                service.as_str()
109            ),
110            next_action: format!(
111                "Configure or publish at least one endpoint for Service Reference `{}`, then retry.",
112                service.as_str()
113            ),
114        }
115    }
116
117    fn invalid_state(message: impl Into<String>) -> Self {
118        Self {
119            code: EndpointResolutionErrorCode::InvalidEndpointState,
120            message: message.into(),
121            next_action: "Publish a non-empty endpoint state whose Service Reference matches its registry key."
122                .to_owned(),
123        }
124    }
125}
126
127impl fmt::Display for EndpointResolutionError {
128    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
129        write!(formatter, "{}", self.message)
130    }
131}
132
133impl Error for EndpointResolutionError {}
134
135pub trait EndpointResolver {
136    fn resolve(&self, service: &ServiceReference)
137    -> Result<EndpointState, EndpointResolutionError>;
138}
139
140#[derive(Debug, Clone)]
141pub struct StaticEndpointResolver {
142    states: BTreeMap<ServiceReference, EndpointState>,
143}
144
145impl StaticEndpointResolver {
146    pub fn new(
147        states: impl IntoIterator<Item = EndpointState>,
148    ) -> Result<Self, EndpointResolutionError> {
149        let mut configured = BTreeMap::new();
150        for state in states {
151            validate_state(&state)?;
152            let service = state.service.clone();
153            if configured.insert(service.clone(), state).is_some() {
154                return Err(EndpointResolutionError::invalid_state(format!(
155                    "Static endpoint configuration contains Service Reference `{}` more than once.",
156                    service.as_str()
157                )));
158            }
159        }
160        Ok(Self { states: configured })
161    }
162}
163
164impl EndpointResolver for StaticEndpointResolver {
165    fn resolve(
166        &self,
167        service: &ServiceReference,
168    ) -> Result<EndpointState, EndpointResolutionError> {
169        self.states
170            .get(service)
171            .cloned()
172            .ok_or_else(|| EndpointResolutionError::no_usable_state(service))
173    }
174}
175
176#[derive(Debug, Clone, Default)]
177/// In-process registry for a local development supervisor that starts and
178/// observes Autonomous Service Workloads on the same machine.
179pub struct LocalProcessEndpointResolver {
180    states: Arc<RwLock<BTreeMap<ServiceReference, EndpointState>>>,
181}
182
183impl LocalProcessEndpointResolver {
184    #[must_use]
185    pub fn new() -> Self {
186        Self::default()
187    }
188
189    pub fn publish(&self, state: EndpointState) -> Result<(), EndpointResolutionError> {
190        validate_state(&state)?;
191        self.states
192            .write()
193            .expect("local process endpoint registry lock poisoned")
194            .insert(state.service.clone(), state);
195        Ok(())
196    }
197
198    pub fn remove(&self, service: &ServiceReference) -> Option<EndpointState> {
199        self.states
200            .write()
201            .expect("local process endpoint registry lock poisoned")
202            .remove(service)
203    }
204}
205
206impl EndpointResolver for LocalProcessEndpointResolver {
207    fn resolve(
208        &self,
209        service: &ServiceReference,
210    ) -> Result<EndpointState, EndpointResolutionError> {
211        self.states
212            .read()
213            .expect("local process endpoint registry lock poisoned")
214            .get(service)
215            .cloned()
216            .ok_or_else(|| EndpointResolutionError::no_usable_state(service))
217    }
218}
219
220#[derive(Debug, Clone)]
221/// Client-side resolver that keeps the last valid state from any resolver
222/// adapter, so source or System Plane availability is never request-path state.
223pub struct LastValidEndpointResolver<R> {
224    source: R,
225    last_valid: Arc<RwLock<BTreeMap<ServiceReference, EndpointState>>>,
226}
227
228impl<R> LastValidEndpointResolver<R> {
229    #[must_use]
230    pub fn new(source: R) -> Self {
231        Self {
232            source,
233            last_valid: Arc::new(RwLock::new(BTreeMap::new())),
234        }
235    }
236}
237
238impl<R: EndpointResolver> EndpointResolver for LastValidEndpointResolver<R> {
239    fn resolve(
240        &self,
241        service: &ServiceReference,
242    ) -> Result<EndpointState, EndpointResolutionError> {
243        match self.source.resolve(service) {
244            Ok(state) => {
245                if validate_state_for(service, &state).is_ok() {
246                    self.last_valid
247                        .write()
248                        .expect("last valid endpoint state lock poisoned")
249                        .insert(service.clone(), state.clone());
250                    Ok(state)
251                } else {
252                    self.cached_or_unavailable(service)
253                }
254            }
255            Err(_) => self.cached_or_unavailable(service),
256        }
257    }
258}
259
260impl<R> LastValidEndpointResolver<R> {
261    fn cached_or_unavailable(
262        &self,
263        service: &ServiceReference,
264    ) -> Result<EndpointState, EndpointResolutionError> {
265        self.last_valid
266            .read()
267            .expect("last valid endpoint state lock poisoned")
268            .get(service)
269            .cloned()
270            .ok_or_else(|| EndpointResolutionError::no_usable_state(service))
271    }
272}
273
274fn validate_state(state: &EndpointState) -> Result<(), EndpointResolutionError> {
275    validate_state_for(&state.service, state)
276}
277
278fn validate_state_for(
279    service: &ServiceReference,
280    state: &EndpointState,
281) -> Result<(), EndpointResolutionError> {
282    if state.service != *service {
283        return Err(EndpointResolutionError::invalid_state(
284            "Endpoint state Service Reference does not match the requested Service Reference.",
285        ));
286    }
287    if service.as_str().trim().is_empty() || state.endpoints.is_empty() {
288        return Err(EndpointResolutionError::invalid_state(
289            "Endpoint state requires a non-empty Service Reference and at least one endpoint.",
290        ));
291    }
292    if state
293        .endpoints
294        .iter()
295        .any(|endpoint| endpoint.address.trim().is_empty())
296    {
297        return Err(EndpointResolutionError::invalid_state(
298            "Endpoint addresses must not be empty.",
299        ));
300    }
301    Ok(())
302}