@@ -49,6 +49,7 @@ use openshell_core::proto_struct::{
4949 deserialize_optional_non_empty_string_list, struct_to_json_value,
5050} ;
5151use openshell_core:: { Config , Error , Result as CoreResult } ;
52+ use std:: cmp:: Ordering ;
5253use std:: collections:: { HashMap , HashSet } ;
5354use std:: io:: Read ;
5455use 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+
26942820fn 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+
28102943fn container_state_needs_shutdown_stop ( state : ContainerSummaryStateEnum ) -> bool {
28112944 matches ! (
28122945 state,
0 commit comments