@@ -24,6 +24,7 @@ package servicediscovery
2424import (
2525 "context"
2626 "encoding/json"
27+ "errors"
2728 "fmt"
2829 "os"
2930 "sort"
@@ -74,18 +75,30 @@ const (
7475 elasticNetworkInterfaceAttachment = "ElasticNetworkInterface"
7576)
7677
77- // The ceilings the waits run against. Each is generous against what the
78- // operation takes in practice — a namespace settles in well under a minute, a
79- // task reaches RUNNING in a couple, and deregistration follows the task
78+ // itestBudget is the wall clock the whole test runs against — the assertions
79+ // and the teardown alike, since every wait below derives from a context
80+ // carrying it. It sits far enough inside the test binary's timeout and the CI
81+ // job's ceiling that a run which goes wrong fails with a diagnostic of its own
82+ // and still reaches its cleanups, rather than being killed mid-test with a live
83+ // Fargate task and a VPC left in the account.
84+ //
85+ // The step ceilings below are sized to fit inside it: 2+4+2+2+2 = 12 minutes of
86+ // assertions, and a teardown of at most the drain ceiling plus seven cleanup
87+ // ceilings (2m + 7×45s ≈ 7m15s), for a worst case of about 19m15s.
88+ const itestBudget = 20 * time .Minute
89+
90+ // The ceilings the individual waits run against. Each is generous against what
91+ // the operation takes in practice — a namespace settles in well under a minute,
92+ // a task reaches RUNNING in a couple, and deregistration follows the task
8093// stopping — and every one of them fails the test when it runs out, so a
8194// contract that never holds is a failure rather than a pass that waited.
8295const (
83- itestNamespaceTimeout = 4 * time .Minute
84- itestTaskRunningTimeout = 6 * time .Minute
85- itestRegistrationTimeout = 3 * time .Minute
86- itestServiceDrainTimeout = 5 * time .Minute
87- itestDeregistrationTimeout = 5 * time .Minute
88- itestCleanupTimeout = 2 * time .Minute
96+ itestNamespaceTimeout = 2 * time .Minute
97+ itestTaskRunningTimeout = 4 * time .Minute
98+ itestRegistrationTimeout = 2 * time .Minute
99+ itestServiceDrainTimeout = 2 * time .Minute
100+ itestDeregistrationTimeout = 2 * time .Minute
101+ itestCleanupTimeout = 45 * time .Second
89102 itestPollInterval = 5 * time .Second
90103 itestNamespacePollInterval = 3 * time .Second
91104)
@@ -100,7 +113,11 @@ func TestPrivateDnsNamespace_Integration_ECSRegistersAndDeregistersTask(t *testi
100113 t .Skip ("AWS_REGION not set; skipping integration test" )
101114 }
102115
103- ctx := context .Background ()
116+ // Registered before any other cleanup, so it is released last: the cleanups
117+ // run against this same context and need it to still be live.
118+ ctx , cancel := context .WithTimeout (context .Background (), itestBudget )
119+ t .Cleanup (cancel )
120+
104121 cfg := & config.Config {Region : region }
105122 awsCfg , err := cfg .ToAwsConfig (ctx )
106123 require .NoError (t , err , "loading AWS config" )
@@ -145,7 +162,7 @@ func TestPrivateDnsNamespace_Integration_ECSRegistersAndDeregistersTask(t *testi
145162 if ecsServiceDeleted {
146163 return
147164 }
148- if err := deleteECSService (context . Background () , ecsClient , clusterARN , names .ecsService ); err != nil {
165+ if err := deleteECSService (ctx , ecsClient , clusterARN , names .ecsService ); err != nil {
149166 t .Logf ("warning: deleting ECS service %s: %v" , names .ecsService , err )
150167 }
151168 })
@@ -234,7 +251,7 @@ func setupNetwork(t *testing.T, ctx context.Context, client *ec2.Client, names i
234251 require .NoError (t , err , "creating VPC" )
235252 vpcID := aws .ToString (vpcOut .Vpc .VpcId )
236253 t .Cleanup (func () {
237- deleteWithRetry (t , "VPC " + vpcID , func (ctx context.Context ) error {
254+ deleteWithRetry (t , ctx , "VPC " + vpcID , func (ctx context.Context ) error {
238255 _ , err := client .DeleteVpc (ctx , & ec2.DeleteVpcInput {VpcId : aws .String (vpcID )})
239256 return err
240257 })
@@ -259,7 +276,7 @@ func setupNetwork(t *testing.T, ctx context.Context, client *ec2.Client, names i
259276 require .NoError (t , err , "creating internet gateway" )
260277 igwID := aws .ToString (igwOut .InternetGateway .InternetGatewayId )
261278 t .Cleanup (func () {
262- deleteWithRetry (t , "internet gateway " + igwID , func (ctx context.Context ) error {
279+ deleteWithRetry (t , ctx , "internet gateway " + igwID , func (ctx context.Context ) error {
263280 if _ , err := client .DetachInternetGateway (ctx , & ec2.DetachInternetGatewayInput {
264281 InternetGatewayId : aws .String (igwID ),
265282 VpcId : aws .String (vpcID ),
@@ -300,7 +317,7 @@ func setupNetwork(t *testing.T, ctx context.Context, client *ec2.Client, names i
300317 t .Cleanup (func () {
301318 // A stopped task's ENI is released after the task itself is gone, so the
302319 // subnet delete is retried until the release lands.
303- deleteWithRetry (t , "subnet " + subnetID , func (ctx context.Context ) error {
320+ deleteWithRetry (t , ctx , "subnet " + subnetID , func (ctx context.Context ) error {
304321 _ , err := client .DeleteSubnet (ctx , & ec2.DeleteSubnetInput {SubnetId : aws .String (subnetID )})
305322 return err
306323 })
@@ -367,7 +384,7 @@ func setupCluster(t *testing.T, ctx context.Context, client *ecs.Client, names i
367384 require .NoError (t , err , "creating ECS cluster %s" , names .cluster )
368385 clusterARN := aws .ToString (out .Cluster .ClusterArn )
369386 t .Cleanup (func () {
370- deleteWithRetry (t , "ECS cluster " + clusterARN , func (ctx context.Context ) error {
387+ deleteWithRetry (t , ctx , "ECS cluster " + clusterARN , func (ctx context.Context ) error {
371388 _ , err := client .DeleteCluster (ctx , & ecs.DeleteClusterInput {Cluster : aws .String (clusterARN )})
372389 return err
373390 })
@@ -397,7 +414,7 @@ func setupTaskDefinition(t *testing.T, ctx context.Context, client *ecs.Client,
397414 require .NoError (t , err , "registering task definition %s" , names .taskDefinition )
398415 taskDefinitionARN := aws .ToString (out .TaskDefinition .TaskDefinitionArn )
399416 t .Cleanup (func () {
400- deleteWithRetry (t , "task definition " + taskDefinitionARN , func (ctx context.Context ) error {
417+ deleteWithRetry (t , ctx , "task definition " + taskDefinitionARN , func (ctx context.Context ) error {
401418 _ , err := client .DeregisterTaskDefinition (ctx , & ecs.DeregisterTaskDefinitionInput {
402419 TaskDefinition : aws .String (taskDefinitionARN ),
403420 })
@@ -446,7 +463,7 @@ func setupNamespace(
446463 require .NotEmpty (t , namespaceID , "namespace create returned no native id" )
447464
448465 t .Cleanup (func () {
449- deleteNamespace (t , provisioner , namespaceID )
466+ deleteNamespace (t , ctx , provisioner , namespaceID )
450467 })
451468
452469 require .NoError (t ,
@@ -459,9 +476,8 @@ func setupNamespace(
459476// delete to settle. The provisioner re-issues a delete Cloud Map rejects while
460477// the namespace still holds resources, so this tolerates a Cloud Map service
461478// whose own delete has not landed yet.
462- func deleteNamespace (t * testing.T , provisioner * PrivateDnsNamespace , namespaceID string ) {
479+ func deleteNamespace (t * testing.T , ctx context. Context , provisioner * PrivateDnsNamespace , namespaceID string ) {
463480 t .Helper ()
464- ctx := context .Background ()
465481 deleteResult , err := provisioner .Delete (ctx , & resource.DeleteRequest {
466482 NativeID : namespaceID ,
467483 ResourceType : resourceType ,
@@ -474,7 +490,7 @@ func deleteNamespace(t *testing.T, provisioner *PrivateDnsNamespace, namespaceID
474490 return
475491 }
476492 if err := awaitProvisionerSuccess (
477- ctx , provisioner , namespaceID , deleteResult .ProgressResult .RequestID , itestNamespaceTimeout ,
493+ ctx , provisioner , namespaceID , deleteResult .ProgressResult .RequestID , itestCleanupTimeout ,
478494 ); err != nil {
479495 t .Logf ("warning: waiting for namespace %s to be deleted: %v" , namespaceID , err )
480496 }
@@ -507,7 +523,9 @@ func awaitProvisionerSuccess(
507523 case resource .OperationStatusSuccess :
508524 return true , nil
509525 case resource .OperationStatusFailure :
510- return false , fmt .Errorf ("namespace %s reported failure: %s" , namespaceID , progress .StatusMessage )
526+ return false , & terminalError {
527+ err : fmt .Errorf ("namespace %s reported failure: %s" , namespaceID , progress .StatusMessage ),
528+ }
511529 default :
512530 return false , nil
513531 }
@@ -545,7 +563,7 @@ func setupCloudMapService(
545563 t .Cleanup (func () {
546564 // Cloud Map rejects the delete while instances are still registered, so
547565 // this is retried for as long as deregistration may take.
548- deleteWithRetry (t , "Cloud Map service " + serviceID , func (ctx context.Context ) error {
566+ deleteWithRetry (t , ctx , "Cloud Map service " + serviceID , func (ctx context.Context ) error {
549567 _ , err := client .DeleteService (ctx , & servicediscoverysdk.DeleteServiceInput {
550568 Id : aws .String (serviceID ),
551569 })
@@ -713,9 +731,8 @@ func deleteECSService(ctx context.Context, client *ecs.Client, cluster, service
713731// one that never does is reported rather than failing the test: the assertions
714732// have already run by then, and a leftover resource is the account sweep's to
715733// reap.
716- func deleteWithRetry (t * testing.T , description string , remove func (context.Context ) error ) {
734+ func deleteWithRetry (t * testing.T , ctx context. Context , description string , remove func (context.Context ) error ) {
717735 t .Helper ()
718- ctx := context .Background ()
719736 var lastErr error
720737 err := waitFor (ctx , itestCleanupTimeout , itestPollInterval , func (ctx context.Context ) (bool , error ) {
721738 lastErr = remove (ctx )
@@ -726,31 +743,59 @@ func deleteWithRetry(t *testing.T, description string, remove func(context.Conte
726743 }
727744}
728745
729- // waitFor calls probe every interval until it reports done, reports an error, or
730- // the timeout runs out. A probe that reports neither done nor an error is one
731- // whose condition has not been reached yet.
746+ // terminalError marks a probe error retrying cannot resolve — an operation AWS
747+ // has already reported as failed — so waitFor reports it at once instead of
748+ // polling out the rest of its ceiling.
749+ type terminalError struct {
750+ err error
751+ }
752+
753+ func (e * terminalError ) Error () string { return e .err .Error () }
754+
755+ func (e * terminalError ) Unwrap () error { return e .err }
756+
757+ // waitFor calls probe every interval until it reports done, reports a
758+ // terminalError, or the timeout runs out. A probe that reports neither done nor
759+ // an error is one whose condition has not been reached yet.
760+ //
761+ // Any other probe error is treated as transient and retried within the ceiling,
762+ // so a single throttled call does not fail the test; an error that persists is
763+ // reported alongside the timeout.
732764func waitFor (
733765 ctx context.Context ,
734766 timeout time.Duration ,
735767 interval time.Duration ,
736768 probe func (context.Context ) (bool , error ),
737769) error {
738770 deadline := time .Now ().Add (timeout )
771+ var lastErr error
739772 for {
740773 done , err := probe (ctx )
741- if err != nil {
774+ var terminal * terminalError
775+ if errors .As (err , & terminal ) {
742776 return err
743777 }
744- if done {
778+ if err == nil && done {
745779 return nil
746780 }
781+ lastErr = err
747782 if time .Now ().After (deadline ) {
748- return fmt .Errorf ("timed out after %s" , timeout )
783+ return withLastProbeError ( fmt .Errorf ("timed out after %s" , timeout ), lastErr )
749784 }
750785 select {
751786 case <- ctx .Done ():
752- return ctx .Err ()
787+ return withLastProbeError ( ctx .Err (), lastErr )
753788 case <- time .After (interval ):
754789 }
755790 }
756791}
792+
793+ // withLastProbeError appends the error the last probe reported, if any, to the
794+ // reason the wait gave up, so a wait that ran out against a persistently failing
795+ // call says what was failing.
796+ func withLastProbeError (err error , lastErr error ) error {
797+ if lastErr == nil {
798+ return err
799+ }
800+ return fmt .Errorf ("%w (last probe error: %v)" , err , lastErr )
801+ }
0 commit comments