@@ -32,6 +32,8 @@ const (
3232 defaultTemplateVersionBuildBackoffAfter = 2 * time .Minute
3333 defaultTemplateVersionBuildInitialPoll = 2 * time .Second
3434 defaultTemplateVersionBuildMaxPollInterval = 10 * time .Second
35+ // MaxTemplateVersionBuildWaitTimeout keeps template build waits within aggregated API request deadlines.
36+ MaxTemplateVersionBuildWaitTimeout = 30 * time .Minute
3537)
3638
3739type templateVersionBuildWaitConfig struct {
5052 _ rest.GracefulDeleter = (* TemplateStorage )(nil )
5153 _ rest.Scoper = (* TemplateStorage )(nil )
5254 _ rest.SingularNameProvider = (* TemplateStorage )(nil )
55+
56+ errTemplateVersionBuildWaitTimeoutExceeded = errors .New ("template version build wait timeout exceeded" )
5357)
5458
5559// TemplateStorage provides codersdk-backed CoderTemplate objects.
@@ -687,7 +691,14 @@ func mapTemplateVersionBuildWaitError(waitErr error, templateName string) error
687691
688692 var timeoutErr * templateVersionBuildWaitTimeoutError
689693 if errors .As (waitErr , & timeoutErr ) {
690- return apierrors .NewBadRequest (timeoutErr .Error ())
694+ switch {
695+ case errors .Is (timeoutErr .cause , errTemplateVersionBuildWaitTimeoutExceeded ):
696+ return apierrors .NewBadRequest (timeoutErr .Error ())
697+ case errors .Is (timeoutErr .cause , context .DeadlineExceeded ), errors .Is (timeoutErr .cause , context .Canceled ):
698+ return apierrors .NewTimeoutError (timeoutErr .Error (), 0 )
699+ default :
700+ return apierrors .NewInternalError (timeoutErr )
701+ }
691702 }
692703
693704 return coder .MapCoderError (waitErr , aggregationv1alpha1 .Resource ("codertemplates" ), templateName )
@@ -709,7 +720,15 @@ func waitForTemplateVersionBuild(ctx context.Context, sdk *codersdk.Client, vers
709720 return waitConfigErr
710721 }
711722
712- waitCtx , cancel := context .WithTimeout (ctx , waitConfig .waitTimeout )
723+ effectiveWaitTimeout := waitConfig .waitTimeout
724+ if requestDeadline , hasRequestDeadline := ctx .Deadline (); hasRequestDeadline {
725+ remaining := time .Until (requestDeadline )
726+ if remaining > 0 && remaining < effectiveWaitTimeout {
727+ effectiveWaitTimeout = remaining
728+ }
729+ }
730+
731+ waitCtx , cancel := context .WithTimeout (ctx , effectiveWaitTimeout )
713732 defer cancel ()
714733
715734 pollInterval := waitConfig .initialPoll
@@ -721,12 +740,17 @@ func waitForTemplateVersionBuild(ctx context.Context, sdk *codersdk.Client, vers
721740 version , err := sdk .TemplateVersion (waitCtx , versionID )
722741 if err != nil {
723742 if waitCtx .Err () != nil {
743+ timeoutCause , timeoutCauseErr := templateVersionBuildWaitTimeoutCause (ctx , waitCtx )
744+ if timeoutCauseErr != nil {
745+ return timeoutCauseErr
746+ }
747+
724748 return & templateVersionBuildWaitTimeoutError {
725749 versionID : versionID ,
726- waitTimeout : waitConfig . waitTimeout ,
750+ waitTimeout : effectiveWaitTimeout ,
727751 lastStatus : lastStatus ,
728752 lastError : lastError ,
729- cause : waitCtx . Err () ,
753+ cause : timeoutCause ,
730754 }
731755 }
732756 return fmt .Errorf ("fetch template version %q: %w" , versionID .String (), err )
@@ -771,21 +795,47 @@ func waitForTemplateVersionBuild(ctx context.Context, sdk *codersdk.Client, vers
771795
772796 select {
773797 case <- waitCtx .Done ():
774- if waitCtx .Err () == nil {
775- return fmt .Errorf ("assertion failed: wait context finished without an error" )
798+ timeoutCause , timeoutCauseErr := templateVersionBuildWaitTimeoutCause (ctx , waitCtx )
799+ if timeoutCauseErr != nil {
800+ return timeoutCauseErr
776801 }
802+
777803 return & templateVersionBuildWaitTimeoutError {
778804 versionID : versionID ,
779- waitTimeout : waitConfig . waitTimeout ,
805+ waitTimeout : effectiveWaitTimeout ,
780806 lastStatus : lastStatus ,
781807 lastError : lastError ,
782- cause : waitCtx . Err () ,
808+ cause : timeoutCause ,
783809 }
784810 case <- time .After (pollInterval ):
785811 }
786812 }
787813}
788814
815+ func templateVersionBuildWaitTimeoutCause (requestCtx , waitCtx context.Context ) (error , error ) {
816+ if requestCtx == nil {
817+ return nil , fmt .Errorf ("assertion failed: request context must not be nil" )
818+ }
819+ if waitCtx == nil {
820+ return nil , fmt .Errorf ("assertion failed: wait context must not be nil" )
821+ }
822+
823+ waitErr := waitCtx .Err ()
824+ if waitErr == nil {
825+ return nil , fmt .Errorf ("assertion failed: wait context finished without an error" )
826+ }
827+
828+ if requestErr := requestCtx .Err (); requestErr != nil {
829+ return requestErr , nil
830+ }
831+
832+ if errors .Is (waitErr , context .DeadlineExceeded ) {
833+ return errTemplateVersionBuildWaitTimeoutExceeded , nil
834+ }
835+
836+ return waitErr , nil
837+ }
838+
789839func loadTemplateVersionBuildWaitConfigFromEnv () (templateVersionBuildWaitConfig , error ) {
790840 waitTimeout , waitTimeoutErr := parseDurationEnvOrDefault (templateVersionBuildWaitTimeoutEnv , defaultTemplateVersionBuildWaitTimeout )
791841 if waitTimeoutErr != nil {
@@ -799,6 +849,16 @@ func loadTemplateVersionBuildWaitConfigFromEnv() (templateVersionBuildWaitConfig
799849 )
800850 }
801851
852+ if waitTimeout > MaxTemplateVersionBuildWaitTimeout {
853+ return templateVersionBuildWaitConfig {}, fmt .Errorf (
854+ "assertion failed: %s (%s) must be <= %s (%s)" ,
855+ templateVersionBuildWaitTimeoutEnv ,
856+ waitTimeout ,
857+ "aggregated API request timeout" ,
858+ MaxTemplateVersionBuildWaitTimeout ,
859+ )
860+ }
861+
802862 backoffAfter , backoffAfterErr := parseDurationEnvOrDefault (templateVersionBuildBackoffAfterEnv , defaultTemplateVersionBuildBackoffAfter )
803863 if backoffAfterErr != nil {
804864 return templateVersionBuildWaitConfig {}, backoffAfterErr
0 commit comments