Skip to main content

rustlavel_discovery/
replicate.rs

1//! Keeping several registries in agreement.
2//!
3//! Registry nodes are peers, not a cluster with a leader. A write arrives at
4//! whichever node the client reached and is forwarded to the others, so every
5//! node ends up holding the same registrations by repetition rather than by
6//! consensus.
7//!
8//! **This is deliberately not consistent, and that is the right trade.** A
9//! registry is a cache of where things are, and the answer is already slightly
10//! out of date the moment it is given — an instance can die between the read
11//! and the request. Paying for consensus would buy agreement about a fact that
12//! is stale anyway, and would mean a node that loses quorum stops answering:
13//! exactly the outage the registry exists to survive. Eureka chose availability
14//! here for the same reason, and the disagreements resolve themselves within a
15//! heartbeat.
16//!
17//! **The loop is the thing to get right.** A node that forwarded what it
18//! received would send it back to the node it came from, forever. Every
19//! forwarded write therefore carries a marker, and a marked write is applied
20//! and never forwarded on — one hop, never two. Eureka's marker is
21//! `?isReplication=true`, and this honours it so the two can be mixed.
22
23use std::time::Duration;
24
25use rustlavel_client::{Client as Http, RequestBuilder};
26use rustlavel_core::Json;
27use rustlavel_http::Method;
28
29use crate::eureka;
30use crate::instance::{Instance, Status};
31
32/// Eureka's marker, on the query string.
33pub const REPLICATION_QUERY: &str = "isReplication=true";
34
35/// The same marker as a header, for anything that strips query strings on the
36/// way through — a load balancer in front of the registries, usually.
37pub const REPLICATION_HEADER: &str = "x-rustlavel-replication";
38
39/// Something one node did that the others need to know about.
40#[derive(Debug, Clone)]
41pub enum Change {
42    Registered(Instance),
43    /// Heartbeats replicate too, and must.
44    ///
45    /// Without them a peer would hear about an instance once and never again,
46    /// and would evict every instance it did not personally receive the
47    /// registration for — the whole registry emptying on every node but one.
48    Renewed { service: String, id: String },
49    Cancelled { service: String, id: String },
50    StatusChanged { service: String, id: String, status: Status },
51}
52
53impl Change {
54    fn method(&self) -> Method {
55        match self {
56            Change::Registered(_) => Method::Post,
57            Change::Renewed { .. } | Change::StatusChanged { .. } => Method::Put,
58            Change::Cancelled { .. } => Method::Delete,
59        }
60    }
61
62    fn path(&self) -> String {
63        let app = |service: &str| service.to_ascii_uppercase();
64        match self {
65            Change::Registered(instance) => format!("/eureka/apps/{}", app(&instance.service)),
66            Change::Renewed { service, id } | Change::Cancelled { service, id } => {
67                format!("/eureka/apps/{}/{id}", app(service))
68            }
69            Change::StatusChanged { service, id, status } => {
70                format!("/eureka/apps/{}/{id}/status?value={}", app(service), status.as_str())
71            }
72        }
73    }
74
75    fn body(&self) -> Option<Json> {
76        match self {
77            Change::Registered(instance) => {
78                Some(Json::object([("instance", eureka::instance_json(instance))]))
79            }
80            _ => None,
81        }
82    }
83}
84
85/// Whether a request arrived from another node rather than from a client.
86///
87/// Takes the two values already parsed out of the request — the
88/// `isReplication` query parameter and the header — rather than a raw query
89/// string. The server parsed that string once already, and a second parser
90/// here would be a second set of rules about `+`, casing and repeated keys for
91/// the two to eventually disagree about.
92///
93/// A marked write is applied and never forwarded on. Getting this wrong in the
94/// other direction — treating a client's write as replicated — is the quieter
95/// bug: the registration reaches one node and no other, and the service is
96/// findable through one registry out of three.
97pub fn is_replication(marker: Option<&str>, header: Option<&str>) -> bool {
98    [marker, header]
99        .into_iter()
100        .flatten()
101        // Casing varies between clients, and a marker missed is a loop.
102        .any(|value| value.trim().eq_ignore_ascii_case("true"))
103}
104
105/// The other registry nodes.
106#[derive(Clone)]
107pub struct Peers {
108    addresses: Vec<String>,
109    http: Http,
110}
111
112impl Peers {
113    pub fn new(addresses: Vec<String>) -> Peers {
114        Peers {
115            addresses,
116            // Short, and shorter than the heartbeat interval: replication runs
117            // on every write, and a peer that has stopped answering must be
118            // given up on before the next one arrives.
119            http: Http::new().timeout(Duration::from_secs(5)),
120        }
121    }
122
123    /// Drop this node's own address from the list.
124    ///
125    /// Operators paste the same list of nodes into every node's configuration,
126    /// because keeping three different lists correct is how one of them ends up
127    /// wrong. This is what makes that safe.
128    pub fn excluding(mut self, own: &str) -> Peers {
129        let own = own.trim_end_matches('/');
130        self.addresses.retain(|address| address.trim_end_matches('/') != own);
131        self
132    }
133
134    pub fn http(mut self, http: Http) -> Peers {
135        self.http = http;
136        self
137    }
138
139    pub fn addresses(&self) -> &[String] {
140        &self.addresses
141    }
142
143    fn request(&self, peer: &str, change: &Change) -> RequestBuilder {
144        let path = change.path();
145        let separator = if path.contains('?') { '&' } else { '?' };
146        let url = format!("{}{path}{separator}{REPLICATION_QUERY}", peer.trim_end_matches('/'));
147
148        let mut request =
149            self.http.request(change.method(), url).header(REPLICATION_HEADER, "true");
150        if let Some(body) = change.body() {
151            request = request.json(body);
152        }
153        request
154    }
155
156    /// Forward a change to every peer, and say how many took it.
157    ///
158    /// Never fails. A peer that is down is logged and skipped: the client's
159    /// registration has already succeeded here, and failing it because a
160    /// third node is unreachable would make the registry less available than
161    /// the thing it is keeping track of.
162    pub async fn send(&self, change: &Change) -> usize {
163        let mut accepted = 0;
164
165        for peer in &self.addresses {
166            match self.request(peer, change).send().await {
167                Ok(response) if response.is_success() => accepted += 1,
168                Ok(response) => rustlavel_core::warn!(
169                    "discovery: {peer} refused a replicated change: HTTP {}",
170                    response.status
171                ),
172                Err(error) => {
173                    rustlavel_core::warn!("discovery: {peer} is unreachable: {error}")
174                }
175            }
176        }
177
178        accepted
179    }
180
181    /// Forward a change without waiting for the peers.
182    ///
183    /// What the registry's own routes call. Replication must not sit in front
184    /// of the client's response — a node whose peers are slow would become slow
185    /// itself, and a registration that takes a second is a service that starts
186    /// a second late.
187    pub fn spread(&self, change: Change) {
188        if self.addresses.is_empty() {
189            return;
190        }
191        let peers = self.clone();
192        tokio::spawn(async move {
193            peers.send(&change).await;
194        });
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use rustlavel_client::{Fake, FakeResponse};
202
203    fn peers(addresses: &[&str]) -> Peers {
204        Peers::new(addresses.iter().map(|a| a.to_string()).collect())
205    }
206
207    /// The marker is what stops three nodes forwarding one registration to each
208    /// other until somebody restarts them.
209    #[test]
210    fn a_forwarded_change_is_marked_both_ways() {
211        let request = peers(&["http://b:8761"])
212            .request("http://b:8761", &Change::Registered(Instance::new("orders", "h", 80)));
213
214        assert!(request.url().contains(REPLICATION_QUERY), "{}", request.url());
215        assert_eq!(request.headers().get(REPLICATION_HEADER), Some("true"));
216    }
217
218    #[test]
219    fn eureka_s_own_marker_is_recognised() {
220        assert!(is_replication(Some("true"), None));
221        assert!(is_replication(Some("TRUE"), None));
222        assert!(is_replication(None, Some("true")));
223    }
224
225    /// The quieter failure: a client's write mistaken for a replicated one
226    /// reaches one node and no other.
227    #[test]
228    fn an_ordinary_request_is_not_mistaken_for_a_replicated_one() {
229        assert!(!is_replication(None, None));
230        assert!(!is_replication(Some(""), None));
231        assert!(!is_replication(Some("false"), None));
232        assert!(!is_replication(Some("1"), None), "only `true` means true");
233        assert!(!is_replication(None, Some("false")));
234    }
235
236    /// Operators paste the same node list into every node.
237    #[test]
238    fn a_node_does_not_replicate_to_itself() {
239        let peers = peers(&["http://a:8761", "http://b:8761", "http://c:8761"])
240            .excluding("http://b:8761/");
241
242        assert_eq!(peers.addresses(), ["http://a:8761", "http://c:8761"]);
243    }
244
245    #[test]
246    fn each_change_goes_where_a_peer_expects_it() {
247        let peers = peers(&["http://b:8761"]);
248        let url_for = |change: &Change| peers.request("http://b:8761", change).url().to_string();
249        let method_for = |change: &Change| peers.request("http://b:8761", change).method();
250
251        let renew = Change::Renewed { service: "orders".into(), id: "10.0.0.1:8080".into() };
252        assert_eq!(method_for(&renew), Method::Put);
253        assert!(url_for(&renew).starts_with("http://b:8761/eureka/apps/ORDERS/10.0.0.1:8080?"));
254
255        let cancel = Change::Cancelled { service: "orders".into(), id: "10.0.0.1:8080".into() };
256        assert_eq!(method_for(&cancel), Method::Delete);
257
258        // This one already carries a query string, so the marker has to join it
259        // rather than start a second one.
260        let status = Change::StatusChanged {
261            service: "orders".into(),
262            id: "10.0.0.1:8080".into(),
263            status: Status::OutOfService,
264        };
265        let url = url_for(&status);
266        assert!(url.contains("status?value=OUT_OF_SERVICE&isReplication=true"), "{url}");
267        assert_eq!(url.matches('?').count(), 1, "two query strings: {url}");
268    }
269
270    #[tokio::test]
271    async fn a_registration_reaches_every_peer() {
272        let http = Http::new().faking(Fake::new().fallback(FakeResponse::text("")));
273        let fake = std::sync::Arc::clone(http.fake().unwrap());
274        let peers = peers(&["http://b:8761", "http://c:8761"]).http(http);
275
276        let accepted = peers.send(&Change::Registered(Instance::new("orders", "h", 80))).await;
277
278        assert_eq!(accepted, 2);
279        fake.assert_sent("b:8761");
280        fake.assert_sent("c:8761");
281    }
282
283    /// A node must not become less available than the services it is keeping
284    /// track of.
285    #[tokio::test]
286    async fn a_dead_peer_does_not_stop_the_others() {
287        let http = Http::new().faking(
288            Fake::new()
289                .on("dead", FakeResponse::text("boom").status(500))
290                .fallback(FakeResponse::text("")),
291        );
292        let peers = peers(&["http://dead:8761", "http://alive:8761"]).http(http);
293
294        assert_eq!(peers.send(&Change::Registered(Instance::new("orders", "h", 80))).await, 1);
295    }
296
297    #[test]
298    fn a_node_with_no_peers_spawns_nothing() {
299        // No runtime here on purpose: `tokio::spawn` outside one panics, so this
300        // passing is the assertion.
301        peers(&[]).spread(Change::Cancelled { service: "orders".into(), id: "x".into() });
302    }
303}