Skip to main content

pingora_load_balancing/
discovery.rs

1// Copyright 2026 Cloudflare, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Service discovery interface and implementations
16
17use arc_swap::ArcSwap;
18use async_trait::async_trait;
19use http::Extensions;
20use pingora_core::protocols::l4::socket::SocketAddr;
21use pingora_error::Result;
22use std::io::Result as IoResult;
23use std::net::ToSocketAddrs;
24use std::{
25    collections::{BTreeSet, HashMap},
26    sync::Arc,
27};
28
29use crate::Backend;
30
31/// [ServiceDiscovery] is the interface to discover [Backend]s.
32#[async_trait]
33pub trait ServiceDiscovery {
34    /// Return the discovered collection of backends and, optionally, whether
35    /// individual backends are enabled to serve.
36    ///
37    /// Enablement map keys are hashes of the corresponding [`Backend`] values,
38    /// produced with [`std::collections::hash_map::DefaultHasher`]. A backend
39    /// omitted from the map is considered enabled.
40    ///
41    /// Background services drop this future to cancel discovery on shutdown, so
42    /// an implementation must not rely on running to completion once polled.
43    async fn discover(&self) -> Result<(BTreeSet<Backend>, HashMap<u64, bool>)>;
44}
45
46// TODO: add DNS base discovery
47
48/// A static collection of [Backend]s for service discovery.
49#[derive(Default)]
50pub struct Static {
51    backends: ArcSwap<BTreeSet<Backend>>,
52}
53
54impl Static {
55    /// Create a new boxed [Static] service discovery with the given backends.
56    pub fn new(backends: BTreeSet<Backend>) -> Box<Self> {
57        Box::new(Static {
58            backends: ArcSwap::new(Arc::new(backends)),
59        })
60    }
61
62    /// Create a new boxed [Static] from a given iterator of items that implements [ToSocketAddrs].
63    pub fn try_from_iter<A, T: IntoIterator<Item = A>>(iter: T) -> IoResult<Box<Self>>
64    where
65        A: ToSocketAddrs,
66    {
67        let mut upstreams = BTreeSet::new();
68        for addrs in iter.into_iter() {
69            let addrs = addrs.to_socket_addrs()?.map(|addr| Backend {
70                addr: SocketAddr::Inet(addr),
71                weight: 1,
72                ext: Extensions::new(),
73            });
74            upstreams.extend(addrs);
75        }
76        Ok(Self::new(upstreams))
77    }
78
79    /// return the collection to backends
80    pub fn get(&self) -> BTreeSet<Backend> {
81        BTreeSet::clone(&self.backends.load())
82    }
83
84    // Concurrent set/add/remove might race with each other
85    // TODO: use a queue to avoid racing
86
87    // TODO: take an impl iter
88    #[allow(dead_code)]
89    pub(crate) fn set(&self, backends: BTreeSet<Backend>) {
90        self.backends.store(backends.into())
91    }
92
93    #[allow(dead_code)]
94    pub(crate) fn add(&self, backend: Backend) {
95        let mut new = self.get();
96        new.insert(backend);
97        self.set(new)
98    }
99
100    #[allow(dead_code)]
101    pub(crate) fn remove(&self, backend: &Backend) {
102        let mut new = self.get();
103        new.remove(backend);
104        self.set(new)
105    }
106}
107
108#[async_trait]
109impl ServiceDiscovery for Static {
110    async fn discover(&self) -> Result<(BTreeSet<Backend>, HashMap<u64, bool>)> {
111        // no readiness
112        let health = HashMap::new();
113        Ok((self.get(), health))
114    }
115}