Skip to content

Commit 7ffe7d5

Browse files
committed
fix(docker): reconcile duplicate sandbox containers
Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
1 parent 45060f4 commit 7ffe7d5

2 files changed

Lines changed: 293 additions & 67 deletions

File tree

crates/openshell-driver-docker/src/lib.rs

Lines changed: 200 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ use openshell_core::proto_struct::{
4949
deserialize_optional_non_empty_string_list, struct_to_json_value,
5050
};
5151
use openshell_core::{Config, Error, Result as CoreResult};
52+
use std::cmp::Ordering;
5253
use std::collections::{HashMap, HashSet};
5354
use std::io::Read;
5455
use std::net::{IpAddr, SocketAddr};
@@ -641,8 +642,8 @@ impl DockerComputeDriver {
641642

642643
async fn current_snapshots(&self) -> Result<Vec<DriverSandbox>, Status> {
643644
let containers = self.list_managed_container_summaries().await?;
644-
let container_sandboxes = containers
645-
.iter()
645+
let container_sandboxes = preferred_container_summaries_by_sandbox_id(&containers)
646+
.into_values()
646647
.filter_map(|summary| {
647648
sandbox_from_container_summary(summary, self.supervisor_readiness.as_ref())
648649
})
@@ -853,10 +854,10 @@ impl DockerComputeDriver {
853854
task.abort();
854855
}
855856

856-
let Some(container) = self
857-
.find_managed_container_summary(sandbox_id, sandbox_name)
858-
.await?
859-
else {
857+
let containers = self
858+
.find_managed_container_summaries(sandbox_id, sandbox_name)
859+
.await?;
860+
if containers.is_empty() {
860861
if let Some(record) = pending {
861862
let container_name = container_name_for_sandbox(&record.sandbox);
862863
match self
@@ -881,36 +882,48 @@ impl DockerComputeDriver {
881882
}
882883
}
883884
return Ok(false);
884-
};
885-
let Some(target) = summary_container_target(&container) else {
886-
return Ok(pending.is_some());
887-
};
888-
889-
match self
890-
.docker
891-
.remove_container(
892-
&target,
893-
Some(RemoveContainerOptionsBuilder::default().force(true).build()),
894-
)
895-
.await
896-
{
897-
Ok(()) => {
885+
}
886+
let targets = managed_container_targets(&containers);
887+
if targets.is_empty() {
888+
if pending.is_some() {
898889
cleanup_sandbox_token_file_for_delete(sandbox_id, pending.as_ref(), &self.config);
899-
Ok(true)
900890
}
901-
Err(err) if is_not_found_error(&err) => {
902-
cleanup_sandbox_token_file_for_delete(sandbox_id, pending.as_ref(), &self.config);
903-
Ok(pending.is_some())
891+
return Ok(pending.is_some());
892+
}
893+
894+
let mut deleted = false;
895+
let mut failures = Vec::new();
896+
for target in targets {
897+
match self
898+
.docker
899+
.remove_container(
900+
&target,
901+
Some(RemoveContainerOptionsBuilder::default().force(true).build()),
902+
)
903+
.await
904+
{
905+
Ok(()) => deleted = true,
906+
Err(err) if is_not_found_error(&err) => {}
907+
Err(err) => failures.push(format!("{target}: {err}")),
904908
}
905-
Err(err) => Err(internal_status("delete docker sandbox container", err)),
906909
}
910+
911+
if !failures.is_empty() {
912+
return Err(Status::internal(format!(
913+
"delete Docker sandbox containers: {}",
914+
failures.join("; ")
915+
)));
916+
}
917+
918+
cleanup_sandbox_token_file_for_delete(sandbox_id, pending.as_ref(), &self.config);
919+
Ok(deleted || pending.is_some())
907920
}
908921

909922
async fn stop_sandbox_inner(&self, sandbox_id: &str, sandbox_name: &str) -> Result<(), Status> {
910-
let Some(container) = self
911-
.find_managed_container_summary(sandbox_id, sandbox_name)
912-
.await?
913-
else {
923+
let containers = self
924+
.find_managed_container_summaries(sandbox_id, sandbox_name)
925+
.await?;
926+
if containers.is_empty() {
914927
if let Some(record) = self.remove_pending_sandbox(sandbox_id, sandbox_name).await {
915928
if let Some(task) = record.task {
916929
task.abort();
@@ -920,27 +933,44 @@ impl DockerComputeDriver {
920933
return Ok(());
921934
}
922935
return Err(Status::not_found("sandbox not found"));
923-
};
924-
let Some(target) = summary_container_target(&container) else {
936+
}
937+
let targets = managed_container_targets(&containers);
938+
if targets.is_empty() {
925939
return Err(Status::not_found("sandbox container has no id or name"));
926-
};
940+
}
927941

928-
match self
929-
.docker
930-
.stop_container(
931-
&target,
932-
Some(
933-
StopContainerOptionsBuilder::default()
934-
.t(docker_stop_timeout_secs(self.config.stop_timeout_secs))
935-
.build(),
936-
),
937-
)
938-
.await
939-
{
940-
Ok(()) => Ok(()),
941-
Err(err) if is_not_modified_error(&err) => Ok(()),
942-
Err(err) if is_not_found_error(&err) => Err(Status::not_found("sandbox not found")),
943-
Err(err) => Err(internal_status("stop docker sandbox container", err)),
942+
let mut stopped = false;
943+
let mut failures = Vec::new();
944+
for target in targets {
945+
match self
946+
.docker
947+
.stop_container(
948+
&target,
949+
Some(
950+
StopContainerOptionsBuilder::default()
951+
.t(docker_stop_timeout_secs(self.config.stop_timeout_secs))
952+
.build(),
953+
),
954+
)
955+
.await
956+
{
957+
Ok(()) => stopped = true,
958+
Err(err) if is_not_modified_error(&err) => stopped = true,
959+
Err(err) if is_not_found_error(&err) => {}
960+
Err(err) => failures.push(format!("{target}: {err}")),
961+
}
962+
}
963+
964+
if !failures.is_empty() {
965+
return Err(Status::internal(format!(
966+
"stop Docker sandbox containers: {}",
967+
failures.join("; ")
968+
)));
969+
}
970+
if stopped {
971+
Ok(())
972+
} else {
973+
Err(Status::not_found("sandbox not found"))
944974
}
945975
}
946976

@@ -1260,11 +1290,11 @@ impl DockerComputeDriver {
12601290
.map_err(|err| internal_status("list Docker sandbox containers", err))
12611291
}
12621292

1263-
async fn find_managed_container_summary(
1293+
async fn find_managed_container_summaries(
12641294
&self,
12651295
sandbox_id: &str,
12661296
sandbox_name: &str,
1267-
) -> Result<Option<ContainerSummary>, Status> {
1297+
) -> Result<Vec<ContainerSummary>, Status> {
12681298
let mut label_filter_values = Vec::new();
12691299
if !sandbox_id.is_empty() {
12701300
label_filter_values.push(format!("{LABEL_SANDBOX_ID}={sandbox_id}"));
@@ -1285,23 +1315,24 @@ impl DockerComputeDriver {
12851315
.await
12861316
.map_err(|err| internal_status("find Docker sandbox container", err))?;
12871317

1288-
Ok(containers.into_iter().find(|summary| {
1289-
let Some(labels) = summary.labels.as_ref() else {
1290-
return false;
1291-
};
1292-
let namespace_matches = labels
1293-
.get(LABEL_SANDBOX_NAMESPACE)
1294-
.is_some_and(|value| value == &self.config.sandbox_namespace);
1295-
let id_matches = sandbox_id.is_empty()
1296-
|| labels
1297-
.get(LABEL_SANDBOX_ID)
1298-
.is_some_and(|value| value == sandbox_id);
1299-
let name_matches = sandbox_name.is_empty()
1300-
|| labels
1301-
.get(LABEL_SANDBOX_NAME)
1302-
.is_some_and(|value| value == sandbox_name);
1303-
namespace_matches && id_matches && name_matches
1304-
}))
1318+
Ok(matching_managed_container_summaries(
1319+
containers,
1320+
&self.config.sandbox_namespace,
1321+
sandbox_id,
1322+
sandbox_name,
1323+
))
1324+
}
1325+
1326+
async fn find_managed_container_summary(
1327+
&self,
1328+
sandbox_id: &str,
1329+
sandbox_name: &str,
1330+
) -> Result<Option<ContainerSummary>, Status> {
1331+
Ok(self
1332+
.find_managed_container_summaries(sandbox_id, sandbox_name)
1333+
.await?
1334+
.into_iter()
1335+
.max_by(compare_container_summary_preference))
13051336
}
13061337

13071338
async fn ensure_image_available(&self, sandbox_id: &str, image: &str) -> Result<(), Status> {
@@ -2691,6 +2722,101 @@ fn parse_memory_limit(value: &str) -> Result<Option<i64>, Status> {
26912722
Ok(Some((amount * multiplier).round() as i64))
26922723
}
26932724

2725+
fn compare_container_summary_preference(
2726+
left: &ContainerSummary,
2727+
right: &ContainerSummary,
2728+
) -> Ordering {
2729+
let left_running = matches!(left.state, Some(ContainerSummaryStateEnum::RUNNING));
2730+
let right_running = matches!(right.state, Some(ContainerSummaryStateEnum::RUNNING));
2731+
2732+
left_running
2733+
.cmp(&right_running)
2734+
.then_with(|| {
2735+
is_canonical_container_summary(left).cmp(&is_canonical_container_summary(right))
2736+
})
2737+
.then_with(|| left.created.cmp(&right.created))
2738+
// Docker container IDs are unique and make equal-state, equal-time
2739+
// selection deterministic regardless of daemon response order.
2740+
.then_with(|| left.id.cmp(&right.id))
2741+
}
2742+
2743+
fn is_canonical_container_summary(summary: &ContainerSummary) -> bool {
2744+
let Some(labels) = summary.labels.as_ref() else {
2745+
return false;
2746+
};
2747+
let Some(sandbox_id) = labels.get(LABEL_SANDBOX_ID) else {
2748+
return false;
2749+
};
2750+
let Some(sandbox_name) = labels.get(LABEL_SANDBOX_NAME) else {
2751+
return false;
2752+
};
2753+
let expected = container_name_for_sandbox(&DriverSandbox {
2754+
id: sandbox_id.clone(),
2755+
name: sandbox_name.clone(),
2756+
..Default::default()
2757+
});
2758+
summary.names.as_ref().is_some_and(|names| {
2759+
names
2760+
.iter()
2761+
.any(|name| name.trim_start_matches('/') == expected)
2762+
})
2763+
}
2764+
2765+
fn matching_managed_container_summaries(
2766+
containers: Vec<ContainerSummary>,
2767+
sandbox_namespace: &str,
2768+
sandbox_id: &str,
2769+
sandbox_name: &str,
2770+
) -> Vec<ContainerSummary> {
2771+
containers
2772+
.into_iter()
2773+
.filter(|summary| {
2774+
let Some(labels) = summary.labels.as_ref() else {
2775+
return false;
2776+
};
2777+
let namespace_matches = labels
2778+
.get(LABEL_SANDBOX_NAMESPACE)
2779+
.is_some_and(|value| value == sandbox_namespace);
2780+
let id_matches = sandbox_id.is_empty()
2781+
|| labels
2782+
.get(LABEL_SANDBOX_ID)
2783+
.is_some_and(|value| value == sandbox_id);
2784+
let name_matches = sandbox_name.is_empty()
2785+
|| labels
2786+
.get(LABEL_SANDBOX_NAME)
2787+
.is_some_and(|value| value == sandbox_name);
2788+
namespace_matches && id_matches && name_matches
2789+
})
2790+
.collect()
2791+
}
2792+
2793+
fn preferred_container_summaries_by_sandbox_id(
2794+
summaries: &[ContainerSummary],
2795+
) -> HashMap<&str, &ContainerSummary> {
2796+
let mut preferred = HashMap::new();
2797+
for summary in summaries {
2798+
let Some(sandbox_id) = summary
2799+
.labels
2800+
.as_ref()
2801+
.and_then(|labels| labels.get(LABEL_SANDBOX_ID))
2802+
else {
2803+
continue;
2804+
};
2805+
2806+
match preferred.entry(sandbox_id.as_str()) {
2807+
std::collections::hash_map::Entry::Vacant(entry) => {
2808+
entry.insert(summary);
2809+
}
2810+
std::collections::hash_map::Entry::Occupied(mut entry) => {
2811+
if compare_container_summary_preference(summary, entry.get()).is_gt() {
2812+
entry.insert(summary);
2813+
}
2814+
}
2815+
}
2816+
}
2817+
preferred
2818+
}
2819+
26942820
fn sandbox_from_container_summary(
26952821
summary: &ContainerSummary,
26962822
readiness: &dyn SupervisorReadiness,
@@ -2807,6 +2933,13 @@ fn summary_container_target(summary: &ContainerSummary) -> Option<String> {
28072933
.or_else(|| summary_container_name(summary))
28082934
}
28092935

2936+
fn managed_container_targets(summaries: &[ContainerSummary]) -> Vec<String> {
2937+
summaries
2938+
.iter()
2939+
.filter_map(summary_container_target)
2940+
.collect()
2941+
}
2942+
28102943
fn container_state_needs_shutdown_stop(state: ContainerSummaryStateEnum) -> bool {
28112944
matches!(
28122945
state,

0 commit comments

Comments
 (0)