1use std::time::{SystemTime, UNIX_EPOCH};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Status {
13 Up,
14 Down,
15 Starting,
16 OutOfService,
20}
21
22impl Status {
23 pub fn parse(text: &str) -> Status {
24 match text.trim().to_ascii_uppercase().as_str() {
25 "UP" => Status::Up,
26 "STARTING" => Status::Starting,
27 "OUT_OF_SERVICE" => Status::OutOfService,
28 _ => Status::Down,
31 }
32 }
33
34 pub fn as_str(self) -> &'static str {
35 match self {
36 Status::Up => "UP",
37 Status::Down => "DOWN",
38 Status::Starting => "STARTING",
39 Status::OutOfService => "OUT_OF_SERVICE",
40 }
41 }
42
43 pub fn takes_traffic(self) -> bool {
45 self == Status::Up
46 }
47}
48
49#[derive(Debug, Clone)]
51pub struct Instance {
52 pub service: String,
56 pub id: String,
58 pub host: String,
59 pub port: u16,
60 pub secure: bool,
61 pub status: Status,
62 pub metadata: Vec<(String, String)>,
66 pub renew_interval: u64,
72 pub last_seen: u64,
74}
75
76impl Instance {
77 pub fn new(service: impl Into<String>, host: impl Into<String>, port: u16) -> Instance {
78 let host = host.into();
79 let service = service.into().to_ascii_uppercase();
80 Instance {
81 id: format!("{host}:{port}"),
82 service,
83 host,
84 port,
85 secure: false,
86 status: Status::Up,
87 metadata: Vec::new(),
88 renew_interval: 30,
89 last_seen: now(),
90 }
91 }
92
93 pub fn id(mut self, id: impl Into<String>) -> Instance {
94 self.id = id.into();
95 self
96 }
97
98 pub fn secure(mut self, secure: bool) -> Instance {
99 self.secure = secure;
100 self
101 }
102
103 pub fn status(mut self, status: Status) -> Instance {
104 self.status = status;
105 self
106 }
107
108 pub fn meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Instance {
109 self.metadata.push((key.into(), value.into()));
110 self
111 }
112
113 pub fn renew_every(mut self, seconds: u64) -> Instance {
114 self.renew_interval = seconds.max(1);
115 self
116 }
117
118 pub fn url(&self) -> String {
120 let scheme = if self.secure { "https" } else { "http" };
121 format!("{scheme}://{}:{}", self.host, self.port)
122 }
123
124 pub fn zone(&self) -> Option<&str> {
125 self.meta_value("zone")
126 }
127
128 pub fn meta_value(&self, key: &str) -> Option<&str> {
129 self.metadata.iter().find(|(k, _)| k == key).map(|(_, v)| v.as_str())
130 }
131
132 pub fn fresh_at(&self, now: u64) -> bool {
139 now.saturating_sub(self.last_seen) < self.renew_interval * 3
140 }
141}
142
143pub fn now() -> u64 {
144 SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |elapsed| elapsed.as_secs())
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
153 fn an_unknown_status_is_down_rather_than_up() {
154 assert_eq!(Status::parse("UP"), Status::Up);
155 assert_eq!(Status::parse("up"), Status::Up);
156 assert_eq!(Status::parse("OUT_OF_SERVICE"), Status::OutOfService);
157 for rubbish in ["", "MAYBE", "ok", "1", "UNKNOWN"] {
158 assert_eq!(Status::parse(rubbish), Status::Down, "{rubbish:?}");
159 }
160 }
161
162 #[test]
165 fn only_up_takes_traffic() {
166 assert!(Status::Up.takes_traffic());
167 for other in [Status::Down, Status::Starting, Status::OutOfService] {
168 assert!(!other.takes_traffic(), "{:?} was given traffic", other);
169 }
170 }
171
172 #[test]
174 fn freshness_allows_two_missed_heartbeats() {
175 let instance = Instance::new("orders", "10.0.0.1", 8080).renew_every(30);
176 let registered = instance.last_seen;
177
178 assert!(instance.fresh_at(registered + 29), "one interval is not a death");
179 assert!(instance.fresh_at(registered + 89), "two misses is not a death");
180 assert!(!instance.fresh_at(registered + 90), "three misses is");
181 }
182
183 #[test]
184 fn a_service_name_is_upper_cased_to_match_what_eureka_clients_send() {
185 assert_eq!(Instance::new("orders", "h", 1).service, "ORDERS");
186 assert_eq!(Instance::new("Orders", "h", 1).service, "ORDERS");
187 }
188
189 #[test]
190 fn an_instance_knows_its_own_url_and_defaults_its_id_to_host_and_port() {
191 let plain = Instance::new("orders", "10.0.0.1", 8080);
192 assert_eq!(plain.url(), "http://10.0.0.1:8080");
193 assert_eq!(plain.id, "10.0.0.1:8080");
194
195 let tls = Instance::new("orders", "orders.example.com", 443).secure(true);
196 assert_eq!(tls.url(), "https://orders.example.com:443");
197 }
198
199 #[test]
200 fn zone_is_metadata_rather_than_a_field_of_its_own() {
201 let instance = Instance::new("orders", "h", 1).meta("zone", "jakarta-a");
202 assert_eq!(instance.zone(), Some("jakarta-a"));
203 assert_eq!(Instance::new("orders", "h", 1).zone(), None);
204 }
205}