4343import org .apache .commons .lang3 .StringUtils ;
4444import org .springframework .boot .autoconfigure .condition .ConditionalOnProperty ;
4545import org .springframework .data .util .Pair ;
46+ import org .springframework .scheduling .annotation .Scheduled ;
4647import org .springframework .security .core .Authentication ;
4748import org .springframework .stereotype .Component ;
4849import software .amazon .awssdk .regions .Region ;
7172import software .amazon .awssdk .services .ecs .model .Tag ;
7273import software .amazon .awssdk .services .ecs .model .Task ;
7374import software .amazon .awssdk .services .ecs .model .Volume ;
75+ import software .amazon .awssdk .services .sts .StsClient ;
7476
7577import javax .annotation .PostConstruct ;
7678import javax .inject .Inject ;
7779import java .io .IOException ;
7880import java .net .URI ;
7981import java .util .ArrayList ;
8082import java .util .Arrays ;
83+ import java .util .Collection ;
8184import java .util .HashMap ;
8285import java .util .List ;
8386import java .util .Map ;
8487import java .util .Objects ;
8588import java .util .Optional ;
8689import java .util .UUID ;
90+ import java .util .concurrent .TimeUnit ;
8791import java .util .regex .Pattern ;
8892import java .util .stream .Stream ;
8993
@@ -100,6 +104,7 @@ public class EcsBackend extends AbstractContainerBackend {
100104 private static final List <RuntimeValueKey <?>> IGNORED_RUNTIME_VALUES = Arrays .asList (PortMappingsKey .inst , UserGroupsKey .inst );
101105 private static final List <String > STARTING_STATES = List .of ("PROVISIONING" , "PENDING" , "ACTIVATING" );
102106 private static final List <String > STOPPING_STATES = List .of ("DEACTIVATING" , "STOPPING" , "DEPROVISIONING" , "STOPPED" , "DELETED" );
107+ private static final Tag TO_DELETE_TAG = Tag .builder ().key ("openanalytics.eu/sp-to-delete" ).value ("true" ).build ();
103108
104109 private EcsClient ecsClient ;
105110 private Boolean enableCloudWatch ;
@@ -111,6 +116,8 @@ public class EcsBackend extends AbstractContainerBackend {
111116 private int totalWaitMs ;
112117 private String cluster ;
113118 private String defaultRepositoryCredentialsParameter ;
119+ private String region ;
120+ private String accountId ;
114121
115122 @ Inject
116123 private IProxySpecProvider proxySpecProvider ;
@@ -120,10 +127,13 @@ public class EcsBackend extends AbstractContainerBackend {
120127 public void initialize () {
121128 super .initialize ();
122129
123- String region = getProperty (PROPERTY_REGION );
130+ region = getProperty (PROPERTY_REGION );
124131 if (region == null ) {
125132 throw new IllegalStateException ("Error in configuration of ECS backend: proxy.ecs.region not set" );
126133 }
134+ try (StsClient stsClient = StsClient .create ()) {
135+ accountId = stsClient .getCallerIdentity ().account ();
136+ }
127137
128138 ecsClient = EcsClient .builder ()
129139 .region (Region .of (region ))
@@ -256,21 +266,24 @@ public Proxy startContainer(Authentication user, Container initialContainer, Con
256266 }, totalWaitMs , "ECS Task" , 10 , proxy , slog )) serviceReady = true ;
257267 else serviceReady = false ;
258268
269+ if (!serviceReady ) {
270+ throw new ContainerFailedToStartException ("Service failed to start" , null , rContainerBuilder .build ());
271+ }
272+
259273 proxyStartupLogBuilder .containerStarted (initialContainer .getIndex ());
260274
261275 String image = ecsClient .describeTasks (builder -> builder .cluster (cluster ).tasks (taskArn )).tasks ().getFirst ().containers ().getFirst ().image ();
262276 rContainerBuilder .addRuntimeValue (new RuntimeValue (ContainerImageKey .inst , image ), false );
263277
264- if (!serviceReady ) {
265- throw new ContainerFailedToStartException ("Service failed to start" , null , rContainerBuilder .build ());
266- }
267-
268278 Map <Integer , Integer > portBindings = new HashMap <>();
269279 Container rContainer = rContainerBuilder .build ();
270280 Map <String , URI > targets = setupPortMappingExistingProxy (proxy , rContainer , portBindings );
271281 return proxy .toBuilder ().addTargets (targets ).updateContainer (rContainer ).build ();
272282 } catch (ContainerFailedToStartException t ) {
273283 throw t ;
284+ } catch (InterruptedException interruptedException ) {
285+ Thread .currentThread ().interrupt ();
286+ throw new ContainerFailedToStartException ("ECS container failed to start" , interruptedException , rContainerBuilder .build ());
274287 } catch (Throwable throwable ) {
275288 throw new ContainerFailedToStartException ("ECS container failed to start" , throwable , rContainerBuilder .build ());
276289 }
@@ -433,6 +446,10 @@ private List<Secret> getSecrets(EcsSpecExtension specExtension) {
433446
434447 @ Override
435448 protected void doStopProxy (Proxy proxy ) {
449+ if (Thread .currentThread ().isInterrupted ()) {
450+ // use stopProxies on shutdown of ShinyProxy
451+ return ;
452+ }
436453 for (Container container : proxy .getContainers ()) {
437454 String taskArn = container .getRuntimeValue (BackendContainerNameKey .inst );
438455 ecsClient .stopTask (builder -> builder .cluster (cluster ).task (taskArn ));
@@ -449,18 +466,45 @@ protected void doStopProxy(Proxy proxy) {
449466 .cluster (cluster )
450467 .tasks (taskArn ));
451468
452- if (response .hasTasks () && !STOPPING_STATES .contains (response .tasks ().getFirst ().lastStatus ())) {
469+ if (response .hasTasks () && !STOPPING_STATES .contains (response .tasks ().getFirst ().desiredStatus ())) {
453470 return Retrying .FAILURE ;
454471 }
455472 }
456473 return Retrying .SUCCESS ;
457- }, totalWaitMs , "Stopping ECS Task" , 10 , proxy , slog );
474+ }, totalWaitMs , "Stopping ECS Task" , 0 , proxy , slog );
458475
459476 if (!isInactive ) {
460477 slog .warn (proxy , "Container did not get into stopping state" );
461478 }
462479 }
463480
481+ @ Override
482+ public void stopProxies (Collection <Proxy > proxies ) {
483+ // ECS has strict rate limits, on shutdown we don't have enough time to stop all task and proxies
484+ // see https://docs.aws.amazon.com/AmazonECS/latest/APIReference/request-throttling.html
485+ // therefore we stop all tasks (rate limit = 40/s * 120s = 4800 tasks) and tag the TaskDefinitions with a tag (rate limit = 10/s)
486+ String taskDefinitionArnPrefix = String .format ("arn:aws:ecs:%s:%s:task-definition/sp-task-definition-" , region , accountId );
487+ String taskDefinitionArnSuffix = ":1" ;
488+ for (Proxy proxy : proxies ) {
489+ for (Container container : proxy .getContainers ()) {
490+ String taskArn = container .getRuntimeValue (BackendContainerNameKey .inst );
491+ try {
492+ ecsClient .stopTask (builder -> builder .cluster (cluster ).task (taskArn ));
493+ } catch (Exception e ) {
494+ log .warn ("Error stopping task: " , e );
495+ }
496+ try {
497+ ecsClient .tagResource (builder -> builder
498+ .resourceArn (taskDefinitionArnPrefix + proxy .getId () + taskDefinitionArnSuffix )
499+ .tags (TO_DELETE_TAG )
500+ );
501+ } catch (Exception e ) {
502+ log .warn ("Error tagging task definition: " , e );
503+ }
504+ }
505+ }
506+ }
507+
464508 @ Override
465509 protected String getPropertyPrefix () {
466510 return PROPERTY_PREFIX ;
0 commit comments