Skip to main content

pingora_core/services/
mod.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//! The service interface
16//!
17//! A service to the pingora server is just something runs forever until the server is shutting
18//! down.
19//!
20//! Two types of services are particularly useful
21//! - services that are listening to some (TCP) endpoints
22//! - services that are just running in the background.
23
24use async_trait::async_trait;
25use daggy::Walker;
26use daggy::{petgraph::visit::Topo, Dag, NodeIndex};
27use log::{error, info, warn};
28use parking_lot::Mutex;
29use std::borrow::Borrow;
30use std::sync::Arc;
31use std::sync::Weak;
32use std::time::Duration;
33use tokio::sync::watch;
34
35#[cfg(unix)]
36use crate::server::ListenFds;
37use crate::server::RuntimeOpts;
38use crate::server::ShutdownWatch;
39
40pub mod background;
41pub mod listening;
42
43/// A notification channel for signaling when a service has become ready.
44///
45/// Services can use this to notify other services that may depend on them
46/// that they have successfully started and are ready to serve requests.
47///
48/// # Example
49///
50/// ```rust,ignore
51/// use pingora_core::services::ServiceReadyNotifier;
52///
53/// async fn my_service(ready_notifier: ServiceReadyNotifier) {
54///     // Perform initialization...
55///
56///     // Signal that the service is ready
57///     ready_notifier.notify_ready();
58///
59///     // Continue with main service loop...
60/// }
61/// ```
62pub struct ServiceReadyNotifier {
63    sender: watch::Sender<bool>,
64}
65
66impl Drop for ServiceReadyNotifier {
67    /// In the event that the notifier is dropped before notifying that the
68    /// service is ready, we opt to signal ready anyway
69    fn drop(&mut self) {
70        // Ignore errors - if there are no receivers, that's fine
71        let _ = self.sender.send(true);
72    }
73}
74
75impl ServiceReadyNotifier {
76    /// Creates a new ServiceReadyNotifier from a watch sender.
77    /// You will not need to create one of these for normal usage, but being
78    /// able to is useful for testing.
79    pub fn new(sender: watch::Sender<bool>) -> Self {
80        Self { sender }
81    }
82
83    /// Notifies dependent services that this service is ready.
84    ///
85    /// Consumes the notifier to ensure ready is only signaled once.
86    pub fn notify_ready(self) {
87        // Dropping the notifier will signal that the service is ready
88        drop(self);
89    }
90}
91
92/// A receiver for watching when a service becomes ready.
93pub type ServiceReadyWatch = watch::Receiver<bool>;
94
95/// A handle to a service in the server.
96///
97/// This is returned by [`crate::server::Server::add_service()`] and provides
98/// methods to declare that other services depend on this one.
99///
100/// # Example
101///
102/// ```rust,ignore
103/// let db_handle = server.add_service(database_service);
104/// let cache_handle = server.add_service(cache_service);
105///
106/// let api_handle = server.add_service(api_service);
107/// api_handle.add_dependency(&db_handle);
108/// api_handle.add_dependency(&cache_handle);
109/// ```
110#[derive(Debug, Clone)]
111pub struct ServiceHandle {
112    pub(crate) id: NodeIndex,
113    name: String,
114    ready_watch: ServiceReadyWatch,
115    dependencies: Weak<Mutex<DependencyGraph>>,
116}
117
118/// Internal representation of a dependency relationship.
119#[derive(Debug, Clone)]
120pub(crate) struct ServiceDependency {
121    pub name: String,
122    pub ready_watch: ServiceReadyWatch,
123}
124
125impl ServiceHandle {
126    /// Creates a new ServiceHandle with the given ID, name, and readiness watcher.
127    pub(crate) fn new(
128        id: NodeIndex,
129        name: String,
130        ready_watch: ServiceReadyWatch,
131        dependencies: &Arc<Mutex<DependencyGraph>>,
132    ) -> Self {
133        Self {
134            id,
135            name,
136            ready_watch,
137            dependencies: Arc::downgrade(dependencies),
138        }
139    }
140
141    #[cfg(test)]
142    fn get_dependencies(&self) -> Vec<ServiceDependency> {
143        let Some(deps_lock) = self.dependencies.upgrade() else {
144            return Vec::new();
145        };
146
147        let deps = deps_lock.lock();
148        deps.get_dependencies(self.id)
149    }
150
151    /// Returns the name of the service.
152    pub fn name(&self) -> &str {
153        &self.name
154    }
155
156    /// Returns a clone of the readiness watcher for this service.
157    #[allow(dead_code)]
158    pub(crate) fn ready_watch(&self) -> ServiceReadyWatch {
159        self.ready_watch.clone()
160    }
161
162    /// Declares that this service depends on another service.
163    ///
164    /// This service will not start until the specified dependency has started
165    /// and signaled readiness.
166    ///
167    /// # Example
168    ///
169    /// ```rust,ignore
170    /// let db_id = server.add_service(database_service);
171    /// let api_id = server.add_service(api_service);
172    ///
173    /// // API service depends on database
174    /// api_id.add_dependency(&db_id);
175    /// ```
176    pub fn add_dependency(&self, dependency: impl Borrow<ServiceHandle>) {
177        let Some(deps_lock) = self.dependencies.upgrade() else {
178            warn!("Attempted to add a dependency after the dependency tree was dropped");
179            return;
180        };
181
182        let mut deps = deps_lock.lock();
183        if let Err(e) = deps.add_dependency(self.id, dependency.borrow().id) {
184            error!("Error creating dependency edge: {e}");
185        }
186    }
187
188    /// Declares that this service depends on the given other services.
189    ///
190    /// This service will not start until the specified dependencies have
191    /// started and signaled readiness.
192    ///
193    /// # Example
194    ///
195    /// ```rust,ignore
196    /// let db_id = server.add_service(database_service);
197    /// let cache_id = server.add_service(cache_service);
198    /// let api_id = server.add_service(api_service);
199    ///
200    /// // API service depends on database
201    /// api_id.add_dependencies(&[&db_id, &cache_id]);
202    /// ```
203    pub fn add_dependencies<'a, D>(&self, dependencies: impl IntoIterator<Item = D>)
204    where
205        D: Borrow<ServiceHandle> + 'a,
206    {
207        for dependency in dependencies {
208            self.add_dependency(dependency);
209        }
210    }
211}
212
213/// Helper for validating service dependency graphs using daggy.
214pub(crate) struct DependencyGraph {
215    /// The directed acyclic graph structure from daggy.
216    dag: Dag<ServiceDependency, ()>,
217}
218
219impl DependencyGraph {
220    /// Creates a new dependency graph.
221    pub(crate) fn new() -> Self {
222        Self { dag: Dag::new() }
223    }
224
225    /// Adds a service node to the graph.
226    ///
227    /// This should be called for all services first, before adding edges.
228    pub(crate) fn add_node(&mut self, name: String, ready_watch: ServiceReadyWatch) -> NodeIndex {
229        self.dag.add_node(ServiceDependency { name, ready_watch })
230    }
231    /// Adds a dependency edge from one service to another.
232    ///
233    /// Returns an error if adding this dependency would create a cycle or reference
234    /// a non-existent service.
235    pub(crate) fn add_dependency(
236        &mut self,
237        dependent_service_node_idx: NodeIndex,
238        dependency_service_node_idx: NodeIndex,
239    ) -> Result<(), String> {
240        // Try to add edge (from dependency to dependent)
241        // daggy will return an error if this would create a cycle
242        if let Err(cycle) =
243            self.dag
244                .add_edge(dependency_service_node_idx, dependent_service_node_idx, ())
245        {
246            return Err(format!(
247                "Circular service dependency detected between {} and {} creating cycle: {cycle}",
248                self.dag[dependency_service_node_idx].name,
249                self.dag[dependent_service_node_idx].name
250            ));
251        }
252
253        Ok(())
254    }
255
256    /// Returns services in topological order (dependencies before dependents).
257    ///
258    /// This ordering ensures that services are started in the correct order.
259    /// Returns service IDs in the correct startup order.
260    pub(crate) fn topological_sort(&self) -> Result<Vec<(NodeIndex, ServiceDependency)>, String> {
261        // Use daggy's built-in topological walker
262        let mut sorted = Vec::new();
263        let mut topo = Topo::new(&self.dag);
264
265        while let Some(service_id) = topo.next(&self.dag) {
266            sorted.push((service_id, self.dag[service_id].clone()));
267        }
268
269        Ok(sorted)
270    }
271
272    pub(crate) fn get_dependencies(&self, service_id: NodeIndex) -> Vec<ServiceDependency> {
273        self.dag
274            .parents(service_id)
275            .iter(&self.dag)
276            .map(|(_, n)| self.dag[n].clone())
277            .collect()
278    }
279}
280
281impl Default for DependencyGraph {
282    fn default() -> Self {
283        Self::new()
284    }
285}
286
287#[async_trait]
288pub trait ServiceWithDependents: Send + Sync {
289    /// This function will be called when the server is ready to start the service.
290    ///
291    /// Override this method if you need to control exactly when the service signals readiness
292    /// (e.g., after async initialization is complete).
293    ///
294    /// # Arguments
295    ///
296    /// - `fds` (Unix only): a collection of listening file descriptors. During zero downtime restart
297    ///   the `fds` would contain the listening sockets passed from the old service, services should
298    ///   take the sockets they need to use then. If the sockets the service looks for don't appear in
299    ///   the collection, the service should create its own listening sockets and then put them into
300    ///   the collection in order for them to be passed to the next server.
301    /// - `shutdown`: the shutdown signal this server would receive.
302    /// - `listeners_per_fd`: number of listener tasks to spawn per file descriptor.
303    /// - `ready_notifier`: notifier to signal when the service is ready. Services with
304    ///   dependents should call `ready_notifier.notify_ready()` once they are fully initialized.
305    async fn start_service(
306        &mut self,
307        #[cfg(unix)] fds: Option<ListenFds>,
308        shutdown: ShutdownWatch,
309        listeners_per_fd: usize,
310        ready_notifier: ServiceReadyNotifier,
311    );
312
313    /// The name of the service, just for logging and naming the threads assigned to this service
314    ///
315    /// Note that due to the limit of the underlying system, only the first 16 chars will be used
316    fn name(&self) -> &str;
317
318    /// The preferred number of threads to run this service
319    ///
320    /// If `None`, the global setting will be used
321    fn threads(&self) -> Option<usize> {
322        None
323    }
324
325    /// Override the runtime options for this service.
326    ///
327    /// Returning [`None`] uses the global runtime options derived from
328    /// [`ServerConf`](crate::server::configuration::ServerConf).
329    fn runtime_opts_override(&self, global: &RuntimeOpts) -> Option<RuntimeOpts> {
330        let _ = global;
331        None
332    }
333
334    /// This is currently called to inform the service about the delay it
335    /// experienced from between waiting on its dependencies. Default behavior
336    /// is to log the time.
337    ///
338    /// TODO. It would be nice if this function was called intermittently by
339    /// the server while the service was waiting to give live updates while the
340    /// service was waiting and allow the service to decide whether to keep
341    /// waiting, continue anyway, or exit
342    fn on_startup_delay(&self, time_waited: Duration) {
343        info!(
344            "Service {} spent {}ms waiting on dependencies",
345            self.name(),
346            time_waited.as_millis()
347        );
348    }
349
350    /// See [`Service::listen_addresses`].
351    fn listen_addresses(&self) -> Option<Vec<String>> {
352        None
353    }
354}
355
356#[async_trait]
357impl<S> ServiceWithDependents for S
358where
359    S: Service,
360{
361    async fn start_service(
362        &mut self,
363        #[cfg(unix)] fds: Option<ListenFds>,
364        shutdown: ShutdownWatch,
365        listeners_per_fd: usize,
366        ready_notifier: ServiceReadyNotifier,
367    ) {
368        // Signal ready immediately
369        ready_notifier.notify_ready();
370
371        S::start_service(
372            self,
373            #[cfg(unix)]
374            fds,
375            shutdown,
376            listeners_per_fd,
377        )
378        .await
379    }
380
381    fn name(&self) -> &str {
382        S::name(self)
383    }
384
385    fn threads(&self) -> Option<usize> {
386        S::threads(self)
387    }
388
389    fn runtime_opts_override(&self, global: &RuntimeOpts) -> Option<RuntimeOpts> {
390        S::runtime_opts_override(self, global)
391    }
392
393    fn on_startup_delay(&self, time_waited: Duration) {
394        S::on_startup_delay(self, time_waited)
395    }
396
397    fn listen_addresses(&self) -> Option<Vec<String>> {
398        S::listen_addresses(self)
399    }
400}
401
402/// The service interface
403#[async_trait]
404pub trait Service: Sync + Send {
405    /// Start the service without readiness notification.
406    ///
407    /// This is a simpler version of [`Self::start_service()`] for services that don't need
408    /// to control when they signal readiness. The default implementation does nothing.
409    ///
410    /// Most services should override this method instead of [`Self::start_service()`].
411    ///
412    /// # Arguments
413    ///
414    /// - `fds` (Unix only): a collection of listening file descriptors.
415    /// - `shutdown`: the shutdown signal this server would receive.
416    /// - `listeners_per_fd`: number of listener tasks to spawn per file descriptor.
417    async fn start_service(
418        &mut self,
419        #[cfg(unix)] _fds: Option<ListenFds>,
420        _shutdown: ShutdownWatch,
421        _listeners_per_fd: usize,
422    ) {
423        // Default: do nothing
424    }
425
426    /// The name of the service, just for logging and naming the threads assigned to this service
427    ///
428    /// Note that due to the limit of the underlying system, only the first 16 chars will be used
429    fn name(&self) -> &str;
430
431    /// The preferred number of threads to run this service
432    ///
433    /// If `None`, the global setting will be used
434    fn threads(&self) -> Option<usize> {
435        None
436    }
437
438    /// Override the runtime options for this service.
439    ///
440    /// Returning [`None`] uses the global runtime options derived from
441    /// [`ServerConf`](crate::server::configuration::ServerConf).
442    fn runtime_opts_override(&self, global: &RuntimeOpts) -> Option<RuntimeOpts> {
443        let _ = global;
444        None
445    }
446
447    /// This is currently called to inform the service about the delay it
448    /// experienced from between waiting on its dependencies. Default behavior
449    /// is to log the time.
450    ///
451    /// TODO. It would be nice if this function was called intermittently by
452    /// the server while the service was waiting to give live updates while the
453    /// service was waiting and allow the service to decide whether to keep
454    /// waiting, continue anyway, or exit
455    fn on_startup_delay(&self, time_waited: Duration) {
456        info!(
457            "Service {} spent {}ms waiting on dependencies",
458            self.name(),
459            time_waited.as_millis()
460        );
461    }
462
463    /// The bind addresses of the listening sockets this service owns.
464    ///
465    /// Addresses must match the keys used for transferred listening fds. Return [`None`] (the
466    /// default) if the service may consume fds without declaring every key; this disables cleanup.
467    fn listen_addresses(&self) -> Option<Vec<String>> {
468        None
469    }
470}
471
472#[cfg(test)]
473mod tests {
474    use super::*;
475
476    #[test]
477    fn test_service_handle_creation() {
478        let deps: Arc<Mutex<DependencyGraph>> = Arc::new(Mutex::new(DependencyGraph::new()));
479        let (tx, rx) = watch::channel(false);
480        let service_id = ServiceHandle::new(0.into(), "test_service".to_string(), rx, &deps);
481
482        assert_eq!(service_id.id, 0.into());
483        assert_eq!(service_id.name(), "test_service");
484
485        // Should be able to clone the watch
486        let watch_clone = service_id.ready_watch();
487        assert!(!*watch_clone.borrow());
488
489        // Signaling ready should be observable through cloned watch
490        tx.send(true).ok();
491        assert!(*watch_clone.borrow());
492    }
493
494    #[test]
495    fn test_service_handle_add_dependency() {
496        let graph: Arc<Mutex<DependencyGraph>> = Arc::new(Mutex::new(DependencyGraph::new()));
497        let (tx1, rx1) = watch::channel(false);
498        let (tx1_clone, rx1_clone) = (tx1.clone(), rx1.clone());
499        let (_tx2, rx2) = watch::channel(false);
500        let (_tx2_clone, rx2_clone) = (_tx2.clone(), rx2.clone());
501
502        // Add nodes to the graph first
503        let dep_node = {
504            let mut g = graph.lock();
505            g.add_node("dependency".to_string(), rx1)
506        };
507        let main_node = {
508            let mut g = graph.lock();
509            g.add_node("main".to_string(), rx2)
510        };
511
512        let dep_service = ServiceHandle::new(dep_node, "dependency".to_string(), rx1_clone, &graph);
513        let main_service = ServiceHandle::new(main_node, "main".to_string(), rx2_clone, &graph);
514
515        // Add dependency
516        main_service.add_dependency(&dep_service);
517
518        // Get dependencies and verify
519        let deps = main_service.get_dependencies();
520        assert_eq!(deps.len(), 1);
521        assert_eq!(deps[0].name, "dependency");
522
523        // Verify watch is working
524        assert!(!*deps[0].ready_watch.borrow());
525        tx1_clone.send(true).ok();
526        assert!(*deps[0].ready_watch.borrow());
527    }
528
529    #[test]
530    fn test_service_handle_multiple_dependencies() {
531        let graph: Arc<Mutex<DependencyGraph>> = Arc::new(Mutex::new(DependencyGraph::new()));
532        let (_tx1, rx1) = watch::channel(false);
533        let rx1_clone = rx1.clone();
534        let (_tx2, rx2) = watch::channel(false);
535        let rx2_clone = rx2.clone();
536        let (_tx3, rx3) = watch::channel(false);
537        let rx3_clone = rx3.clone();
538
539        // Add nodes to the graph first
540        let dep1_node = {
541            let mut g = graph.lock();
542            g.add_node("dep1".to_string(), rx1)
543        };
544        let dep2_node = {
545            let mut g = graph.lock();
546            g.add_node("dep2".to_string(), rx2)
547        };
548        let main_node = {
549            let mut g = graph.lock();
550            g.add_node("main".to_string(), rx3)
551        };
552
553        let dep1 = ServiceHandle::new(dep1_node, "dep1".to_string(), rx1_clone, &graph);
554        let dep2 = ServiceHandle::new(dep2_node, "dep2".to_string(), rx2_clone, &graph);
555        let main_service = ServiceHandle::new(main_node, "main".to_string(), rx3_clone, &graph);
556
557        // Add multiple dependencies
558        main_service.add_dependency(&dep1);
559        main_service.add_dependency(&dep2);
560
561        // Get dependencies and verify
562        let deps = main_service.get_dependencies();
563        assert_eq!(deps.len(), 2);
564
565        let dep_names: Vec<&str> = deps.iter().map(|d| d.name.as_str()).collect();
566        assert!(dep_names.contains(&"dep1"));
567        assert!(dep_names.contains(&"dep2"));
568    }
569
570    #[test]
571    fn test_single_service_no_dependencies() {
572        let mut graph = DependencyGraph::new();
573        let (_tx, rx) = watch::channel(false);
574        let _node = graph.add_node("service1".to_string(), rx);
575
576        let order = graph.topological_sort().unwrap();
577        assert_eq!(order.len(), 1);
578        assert_eq!(order[0].1.name, "service1");
579    }
580
581    #[test]
582    fn test_simple_dependency_chain() {
583        let mut graph = DependencyGraph::new();
584        let (_tx1, rx1) = watch::channel(false);
585        let (_tx2, rx2) = watch::channel(false);
586        let (_tx3, rx3) = watch::channel(false);
587
588        let node1 = graph.add_node("service1".to_string(), rx1);
589        let node2 = graph.add_node("service2".to_string(), rx2);
590        let node3 = graph.add_node("service3".to_string(), rx3);
591
592        // service2 depends on service1, service3 depends on service2
593        graph.add_dependency(node2, node1).unwrap();
594        graph.add_dependency(node3, node2).unwrap();
595
596        let order = graph.topological_sort().unwrap();
597        assert_eq!(order.len(), 3);
598        // Verify order: service1, service2, service3
599        assert_eq!(order[0].1.name, "service1");
600        assert_eq!(order[1].1.name, "service2");
601        assert_eq!(order[2].1.name, "service3");
602    }
603
604    #[test]
605    fn test_diamond_dependency() {
606        let mut graph = DependencyGraph::new();
607        let (_tx1, rx1) = watch::channel(false);
608        let (_tx2, rx2) = watch::channel(false);
609        let (_tx3, rx3) = watch::channel(false);
610
611        let db = graph.add_node("db".to_string(), rx1);
612        let cache = graph.add_node("cache".to_string(), rx2);
613        let api = graph.add_node("api".to_string(), rx3);
614
615        // api depends on both db and cache
616        graph.add_dependency(api, db).unwrap();
617        graph.add_dependency(api, cache).unwrap();
618
619        let order = graph.topological_sort().unwrap();
620        // api should come last, but db and cache order doesn't matter
621        assert_eq!(order.len(), 3);
622        assert_eq!(order[2].1.name, "api");
623        let first_two: Vec<&str> = order[0..2].iter().map(|(_, d)| d.name.as_str()).collect();
624        assert!(first_two.contains(&"db"));
625        assert!(first_two.contains(&"cache"));
626    }
627
628    #[test]
629    #[should_panic(expected = "node indices out of bounds")]
630    fn test_missing_dependency() {
631        let mut graph = DependencyGraph::new();
632        let (_tx1, rx1) = watch::channel(false);
633
634        let node1 = graph.add_node("service1".to_string(), rx1);
635        let nonexistent = NodeIndex::new(999);
636
637        // Try to add dependency on non-existent node - this should panic
638        let _ = graph.add_dependency(node1, nonexistent);
639    }
640
641    #[test]
642    fn test_circular_dependency_self() {
643        let mut graph = DependencyGraph::new();
644        let (_tx1, rx1) = watch::channel(false);
645
646        let node1 = graph.add_node("service1".to_string(), rx1);
647
648        // Try to make service depend on itself
649        let result = graph.add_dependency(node1, node1);
650
651        assert!(result.is_err());
652        assert!(result.unwrap_err().contains("Circular"));
653    }
654
655    #[test]
656    fn test_circular_dependency_two_services() {
657        let mut graph = DependencyGraph::new();
658        let (_tx1, rx1) = watch::channel(false);
659        let (_tx2, rx2) = watch::channel(false);
660
661        // Add both nodes first
662        let node1 = graph.add_node("service1".to_string(), rx1);
663        let node2 = graph.add_node("service2".to_string(), rx2);
664
665        // Try to add circular dependencies
666        graph.add_dependency(node1, node2).unwrap();
667        let result = graph.add_dependency(node2, node1);
668
669        assert!(result.is_err());
670        assert!(result.unwrap_err().contains("Circular"));
671    }
672
673    #[test]
674    fn test_circular_dependency_three_services() {
675        let mut graph = DependencyGraph::new();
676        let (_tx1, rx1) = watch::channel(false);
677        let (_tx2, rx2) = watch::channel(false);
678        let (_tx3, rx3) = watch::channel(false);
679
680        // Add all nodes first
681        let node1 = graph.add_node("service1".to_string(), rx1);
682        let node2 = graph.add_node("service2".to_string(), rx2);
683        let node3 = graph.add_node("service3".to_string(), rx3);
684
685        // Add dependencies that would form a cycle
686        graph.add_dependency(node1, node2).unwrap();
687        graph.add_dependency(node2, node3).unwrap();
688        let result = graph.add_dependency(node3, node1);
689
690        assert!(result.is_err());
691        assert!(result.unwrap_err().contains("Circular"));
692    }
693
694    #[test]
695    fn test_complex_valid_graph() {
696        let mut graph = DependencyGraph::new();
697        let (_tx1, rx1) = watch::channel(false);
698        let (_tx2, rx2) = watch::channel(false);
699        let (_tx3, rx3) = watch::channel(false);
700        let (_tx4, rx4) = watch::channel(false);
701        let (_tx5, rx5) = watch::channel(false);
702
703        // Build a complex dependency graph:
704        //   db, cache - no deps
705        //   auth -> db
706        //   api -> db, cache, auth
707        //   frontend -> api
708        let db = graph.add_node("db".to_string(), rx1);
709        let cache = graph.add_node("cache".to_string(), rx2);
710        let auth = graph.add_node("auth".to_string(), rx3);
711        let api = graph.add_node("api".to_string(), rx4);
712        let frontend = graph.add_node("frontend".to_string(), rx5);
713
714        graph.add_dependency(auth, db).unwrap();
715        graph.add_dependency(api, db).unwrap();
716        graph.add_dependency(api, cache).unwrap();
717        graph.add_dependency(api, auth).unwrap();
718        graph.add_dependency(frontend, api).unwrap();
719
720        let order = graph.topological_sort().unwrap();
721
722        // Verify ordering constraints using names
723        let db_pos = order.iter().position(|(_, d)| d.name == "db").unwrap();
724        let cache_pos = order.iter().position(|(_, d)| d.name == "cache").unwrap();
725        let auth_pos = order.iter().position(|(_, d)| d.name == "auth").unwrap();
726        let api_pos = order.iter().position(|(_, d)| d.name == "api").unwrap();
727        let frontend_pos = order
728            .iter()
729            .position(|(_, d)| d.name == "frontend")
730            .unwrap();
731
732        assert!(db_pos < auth_pos);
733        assert!(auth_pos < api_pos);
734        assert!(db_pos < api_pos);
735        assert!(cache_pos < api_pos);
736        assert!(api_pos < frontend_pos);
737    }
738}