Skip to main content

osproxy_core/
target.rs

1//! Where a routed request is sent.
2//!
3//! A [`Target`] is the physical destination a routing decision resolves to: a
4//! concrete cluster and a concrete index. In v1 every request resolves to
5//! exactly one target, there is no synchronous fan-out (`docs/00` non-goals,
6//! ADR-002). The tenancy layer turns a partition's placement into a `Target`;
7//! the sink and upstream pool consume it.
8
9use std::fmt;
10
11use crate::ids::{ClusterId, IndexName};
12
13/// The physical destination of a single routed request.
14///
15/// Both fields are ids/names (never tenant values), so a `Target` is safe to
16/// render in telemetry and `/debug/explain` (`docs/05` ยง7).
17///
18/// # Examples
19///
20/// ```
21/// use osproxy_core::{ClusterId, IndexName, Target};
22///
23/// let target = Target::new(ClusterId::from("eu-1"), IndexName::from("logs-shared"));
24/// assert_eq!(target.cluster.as_str(), "eu-1");
25/// assert_eq!(target.to_string(), "eu-1/logs-shared");
26/// ```
27#[derive(Clone, Debug)]
28pub struct Target {
29    /// The physical OpenSearch cluster the request is sent to.
30    pub cluster: ClusterId,
31    /// The concrete (physical) index the request operates on.
32    pub index: IndexName,
33    /// The cluster's base URL, supplied by the tenancy as part of the placement
34    /// result (the sink builds a pool for it on first use). `None` only in unit
35    /// tests that dispatch to an in-memory sink, which ignores it.
36    ///
37    /// Excluded from identity (equality/hashing/`Display`): the endpoint is a
38    /// function of the cluster, not part of *which* target this is, so two ops
39    /// for the same `cluster`+`index` stay one demux key regardless of it.
40    pub endpoint: Option<String>,
41}
42
43impl Target {
44    /// Constructs a target from a cluster and an index (no endpoint).
45    #[must_use]
46    pub fn new(cluster: ClusterId, index: IndexName) -> Self {
47        Self {
48            cluster,
49            index,
50            endpoint: None,
51        }
52    }
53
54    /// Sets the cluster's base URL (builder style), as resolved from the
55    /// placement result.
56    #[must_use]
57    pub fn with_endpoint(mut self, endpoint: Option<String>) -> Self {
58        self.endpoint = endpoint;
59        self
60    }
61}
62
63// Identity is (cluster, index) only; the endpoint is dispatch metadata derived
64// from the cluster, so it is deliberately excluded.
65impl PartialEq for Target {
66    fn eq(&self, other: &Self) -> bool {
67        self.cluster == other.cluster && self.index == other.index
68    }
69}
70impl Eq for Target {}
71impl std::hash::Hash for Target {
72    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
73        self.cluster.hash(state);
74        self.index.hash(state);
75    }
76}
77
78impl fmt::Display for Target {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        write!(f, "{}/{}", self.cluster, self.index)
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn target_exposes_cluster_and_index_and_displays_path_like() {
90        let target = Target::new(ClusterId::from("us-2"), IndexName::from("orders-7"));
91        assert_eq!(target.cluster.as_str(), "us-2");
92        assert_eq!(target.index.as_str(), "orders-7");
93        assert_eq!(target.to_string(), "us-2/orders-7");
94    }
95
96    #[test]
97    fn targets_compare_by_both_fields() {
98        let a = Target::new(ClusterId::from("c"), IndexName::from("i"));
99        let b = Target::new(ClusterId::from("c"), IndexName::from("j"));
100        assert_ne!(a, b);
101        assert_eq!(a, a.clone());
102    }
103}