Skip to content

Commit ca59a30

Browse files
committed
resource: handle shared resource starting/stopping
1 parent 0be118d commit ca59a30

1 file changed

Lines changed: 109 additions & 3 deletions

File tree

src/resource.rs

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Copyright 2025. Triad National Security, LLC.
33

44
use 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)]
419444
struct 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

471525
impl 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

Comments
 (0)