22// Copyright 2025. Triad National Security, LLC.
33
44use std:: {
5- collections:: { HashMap , VecDeque } ,
5+ collections:: { HashMap , HashSet , VecDeque } ,
66 sync:: { Arc , Mutex } ,
77} ;
88
@@ -146,6 +146,23 @@ impl ResourceGroup {
146146 self . failover_node . as_ref ( ) . map ( |h| & h. host )
147147 }
148148
149+ /// When beginning to manage a resource group on a given Host, add the Host ID to each
150+ /// resource's list of Hosts that the resource should be running on.
151+ fn apply_group_to_host ( & self , host : & HostId ) {
152+ if !self . get_managed ( ) {
153+ // This doesn't apply to unmanaged resource groups.
154+ return ;
155+ }
156+
157+ self . root . apply_group_to_host ( host, & self . id ( ) ) ;
158+ }
159+
160+ /// When beginning to manage a resource group on a given Host, add the Host ID to each
161+ /// resource's list of Hosts that the resource should be running on.
162+ fn remove_group_from_host ( & self , host : & HostId ) {
163+ self . root . remove_group_from_host ( host, & self . id ( ) ) ;
164+ }
165+
149166 /// The host-driven resource management loop manages resources on a given location until
150167 /// either:
151168 ///
@@ -156,6 +173,8 @@ impl ResourceGroup {
156173 /// returns back to the host management code so that the host can begin checing the
157174 /// failover partner to see if the resource was started there (manual failover).
158175 pub async fn manage_loop ( & self , client : & Client , loc : Location ) -> Result < ( ) , ManagementError > {
176+ self . apply_group_to_host ( & client. name ) ;
177+
159178 loop {
160179 self . update_resources ( client) . await ?;
161180 match self . get_overall_status_on_host ( & client. name ) {
@@ -210,6 +229,8 @@ impl ResourceGroup {
210229
211230 /// Attempt to stop the resources in this resource group.
212231 pub async fn stop_resources ( & self , client : & Client ) -> Result < ( ) , ManagementError > {
232+ self . remove_group_from_host ( & client. name ) ;
233+
213234 let res = self . root . stop_recursive ( client) . await ;
214235
215236 match res {
@@ -251,13 +272,17 @@ impl ResourceGroup {
251272 if let Some ( ref failover_node) = self . failover_node {
252273 * failover_node. state . lock ( ) . unwrap ( ) = State :: Unknown ;
253274 }
275+
276+ // TODO: this should clear per-resource knowledge as well
277+ self . root . clear_started_count ( ) ;
254278 }
255279
256280 * managed_status = managed;
257281 }
258282
259283 pub fn has_been_stopped ( & self , host : & HostId ) {
260284 self . set_host_state ( State :: Stopped , host) ;
285+ self . remove_group_from_host ( host) ;
261286 }
262287
263288 pub fn is_running_nowhere ( & self ) -> bool {
@@ -406,7 +431,7 @@ pub struct Resource {
406431 /// resource has the requirement that it cannot run on multiple hosts at the same time. For
407432 /// such "mutually exclusive" resources, count will equal 1. When count is N, the resource can
408433 /// run on up to N hosts simultanesouly.
409- _count : usize ,
434+ count : usize ,
410435
411436 pub state : ResourceState ,
412437
@@ -417,7 +442,13 @@ pub type HostStatusMap = HashMap<HostId, ResourceStatus>;
417442
418443#[ derive( Clone , Debug , Default ) ]
419444struct PerHostStatus {
445+ /// The resource's status on this Host.
420446 status : ResourceStatus ,
447+
448+ /// The set of parents of this Resource which are running on this Host. This determines whether
449+ /// the resource is supposed to be started / stopped on this Host. When the set is empty, the
450+ /// resource should be stopped. When the set is nonempty, the resource should be started.
451+ started_groups : HashSet < ResourceId > ,
421452}
422453
423454#[ derive( Clone , Debug ) ]
@@ -466,6 +497,29 @@ impl ResourceState {
466497 ent. status = new_status;
467498 old_status. status
468499 }
500+
501+ /// How many times this resource is supposed to be started across a single host. If the result
502+ /// is > 1, it means that multiple parents of this resource are both supposed to be running on
503+ /// this host.
504+ fn started_count_on_host ( & self , host : & HostId ) -> usize {
505+ self . inner
506+ . lock ( )
507+ . unwrap ( )
508+ . get ( host)
509+ . expect ( "Host {host} must have an entry in the status map" )
510+ . started_groups
511+ . len ( )
512+ }
513+
514+ /// How many times this resource is supposed to be started across the entire cluster.
515+ fn started_count ( & self ) -> usize {
516+ self . inner
517+ . lock ( )
518+ . unwrap ( )
519+ . values ( )
520+ . map ( |h| h. started_groups . len ( ) )
521+ . sum ( )
522+ }
469523}
470524
471525impl Resource {
@@ -490,13 +544,59 @@ impl Resource {
490544 kind : res. kind . clone ( ) ,
491545 parameters : res. parameters . clone ( ) ,
492546 dependents,
493- _count : * count,
547+ count : * count,
494548 state : state. clone ( ) ,
495549 id : ResourceId ( res. name . clone ( ) ) ,
496550 args,
497551 }
498552 }
499553
554+ fn apply_group_to_host ( & self , host : & HostId , root : & ResourceId ) {
555+ {
556+ let mut map = self . state . inner . lock ( ) . unwrap ( ) ;
557+ let ent = map
558+ . get_mut ( host)
559+ . expect ( "Host {host} must have an entry in the status map" ) ;
560+ ent. started_groups . insert ( root. clone ( ) ) ;
561+ }
562+
563+ let started_count = self . state . started_count ( ) ;
564+
565+ if started_count > self . count {
566+ panic ! (
567+ "Resource {} is managed too many times: {}, maximum allowed: {}" ,
568+ self . id, started_count, self . count
569+ ) ;
570+ }
571+
572+ for child in & self . dependents {
573+ child. apply_group_to_host ( host, root) ;
574+ }
575+ }
576+
577+ fn remove_group_from_host ( & self , host : & HostId , root : & ResourceId ) {
578+ let mut map = self . state . inner . lock ( ) . unwrap ( ) ;
579+ let ent = map
580+ . get_mut ( host)
581+ . expect ( "Host {host} must have an entry in the status map" ) ;
582+ // TODO: assert that the group is already in the list?
583+ ent. started_groups . remove ( root) ;
584+
585+ for child in & self . dependents {
586+ child. remove_group_from_host ( host, root) ;
587+ }
588+ }
589+
590+ fn clear_started_count ( & self ) {
591+ for per_host_map in self . state . inner . lock ( ) . unwrap ( ) . values_mut ( ) {
592+ std:: mem:: take ( & mut per_host_map. started_groups ) ;
593+ }
594+
595+ for child in & self . dependents {
596+ child. clear_started_count ( ) ;
597+ }
598+ }
599+
500600 /// This method checks if the resource is running on the system connected via the given Client.
501601 pub async fn update_status ( & self , client : & Client ) -> Result < ( ) , ManagementError > {
502602 match self . monitor ( client) . await {
@@ -590,6 +690,12 @@ impl Resource {
590690
591691 get_worst_error ( future:: join_all ( results) . await . into_iter ( ) ) ?;
592692
693+ if self . state . started_count_on_host ( & client. name ) > 0 {
694+ // If this resource still has a started parent on this host, then it shouldn't be
695+ // stopped.
696+ return Ok ( ( ) ) ;
697+ }
698+
593699 match self . stop ( client) . await {
594700 Ok ( AgentReply :: Success ( ocf:: Status :: Success ) ) => {
595701 self . set_status ( ResourceStatus :: Stopped , & client. name ) ;
0 commit comments