@@ -981,6 +981,150 @@ func createFlagsOverride(opts *vm.CreateOpts) {
981981 }
982982}
983983
984+ // createRetryPlanner owns safe mutations between roachprod create attempts.
985+ // The outer create loop still retries all create failures; this planner only
986+ // adds targeted attempt changes when a structured error makes that safe. Today
987+ // it handles zonal capacity errors by changing create zones. Future policies
988+ // can add other mutations here, such as falling back from spot to on-demand.
989+ type createRetryPlanner struct {
990+ clusterSpec spec.ClusterSpec
991+ params spec.RoachprodClusterConfig
992+ arch vm.CPUArch
993+
994+ // explicitZones records whether zones were set by roachtest config or the
995+ // test spec. These zones take precedence over provider defaults and provider
996+ // capacity error suggestions.
997+ explicitZones bool
998+ // retryCandidates is the provider's default non-geo zone pool. It is used as
999+ // a fallback after zonal capacity failures when no untried provider-suggested
1000+ // zone is available.
1001+ retryCandidates []string
1002+ // attemptedZones tracks zones selected for a create attempt or reported as
1003+ // failed by the provider. It prevents retrying a zone just because a provider
1004+ // hint or fallback list points back to it.
1005+ attemptedZones map [string ]struct {}
1006+ // nextAttemptZones is a one-shot zone override selected after a capacity
1007+ // failure for the next create attempt.
1008+ nextAttemptZones []string
1009+ }
1010+
1011+ func newCreateRetryPlanner (
1012+ clusterSpec spec.ClusterSpec , params spec.RoachprodClusterConfig , arch vm.CPUArch ,
1013+ ) * createRetryPlanner {
1014+ explicitZones := clusterSpec .UsesExplicitZones (params )
1015+ var retryCandidates []string
1016+ if ! clusterSpec .Geo && ! explicitZones {
1017+ retryCandidates = spec .DefaultRetryZoneCandidates (params .Cloud , arch )
1018+ }
1019+ return & createRetryPlanner {
1020+ clusterSpec : clusterSpec ,
1021+ params : params ,
1022+ arch : arch ,
1023+ explicitZones : explicitZones ,
1024+ retryCandidates : retryCandidates ,
1025+ attemptedZones : make (map [string ]struct {}),
1026+ }
1027+ }
1028+
1029+ // zonesForAttempt returns the zones for the next roachprod create attempt. It
1030+ // consumes a pending retry override when one exists, otherwise it falls back to
1031+ // the test's ordinary roachprod create zones.
1032+ func (p * createRetryPlanner ) zonesForAttempt () []string {
1033+ var zones []string
1034+ if len (p .nextAttemptZones ) > 0 {
1035+ zones = append ([]string (nil ), p .nextAttemptZones ... )
1036+ p .nextAttemptZones = nil
1037+ } else {
1038+ zones = p .clusterSpec .RoachprodCreateZones (p .params , p .arch )
1039+ }
1040+ for _ , zone := range zones {
1041+ if zone != "" {
1042+ p .attemptedZones [zone ] = struct {}{}
1043+ }
1044+ }
1045+ return zones
1046+ }
1047+
1048+ // recordFailure is called after a roachprod create attempt fails and decides
1049+ // whether the next attempt should change any create inputs. It leaves the
1050+ // ordinary retry loop behavior unchanged for failures it does not recognize or
1051+ // cannot safely plan around. For the current zone retry policy, explicit
1052+ // test/config zones and geo-distributed clusters are left alone because
1053+ // changing their zones would override user intent or require more
1054+ // topology-aware retry planning.
1055+ func (p * createRetryPlanner ) recordFailure (err error , l * logger.Logger ) {
1056+ var capacityErr * vm.CreateCapacityError
1057+ if ! errors .As (err , & capacityErr ) {
1058+ return
1059+ }
1060+ if capacityErr .Provider != "" && capacityErr .Provider != p .params .Cloud .String () {
1061+ return
1062+ }
1063+ if capacityErr .CapacityClass != vm .CreateCapacityClassZone {
1064+ return
1065+ }
1066+ if p .explicitZones {
1067+ l .Printf ("provider reported capacity error, but zones are explicit; keeping requested zones" )
1068+ return
1069+ }
1070+ if p .clusterSpec .Geo {
1071+ l .Printf ("provider reported capacity error, but geo-distributed zone retry overrides are not supported" )
1072+ return
1073+ }
1074+
1075+ retryZones , source := p .selectNextZones (capacityErr )
1076+ if len (retryZones ) == 0 {
1077+ return
1078+ }
1079+ p .nextAttemptZones = retryZones
1080+ machineType := ""
1081+ if capacityErr .MachineType != "" {
1082+ machineType = fmt .Sprintf (" for machine type %s" , capacityErr .MachineType )
1083+ }
1084+ failedZones := strings .Join (capacityErr .FailedZones , "," )
1085+ if failedZones == "" {
1086+ failedZones = "unknown"
1087+ }
1088+ l .Printf ("provider reported capacity error%s in zone(s) [%s]; retrying next cluster creation in zone(s) [%s] from %s" ,
1089+ machineType ,
1090+ failedZones ,
1091+ strings .Join (retryZones , "," ),
1092+ source ,
1093+ )
1094+ }
1095+
1096+ // selectNextZones chooses the zone override for the next create attempt after a
1097+ // zonal capacity failure. Provider-suggested zones are preferred when they have
1098+ // not already been tried; otherwise it falls back to the first untried default
1099+ // retry candidate. The second return value describes the source of the selected
1100+ // zones for logging.
1101+ func (p * createRetryPlanner ) selectNextZones (
1102+ capacityErr * vm.CreateCapacityError ,
1103+ ) ([]string , string ) {
1104+ for _ , zone := range capacityErr .FailedZones {
1105+ if zone != "" {
1106+ p .attemptedZones [zone ] = struct {}{}
1107+ }
1108+ }
1109+ for _ , zone := range capacityErr .SuggestedZones {
1110+ if zone == "" {
1111+ continue
1112+ }
1113+ if _ , ok := p .attemptedZones [zone ]; ok {
1114+ continue
1115+ }
1116+ return []string {zone }, "provider hint"
1117+ }
1118+
1119+ for _ , zone := range p .retryCandidates {
1120+ if _ , ok := p .attemptedZones [zone ]; ok {
1121+ continue
1122+ }
1123+ return []string {zone }, "untried default zones"
1124+ }
1125+ return nil , ""
1126+ }
1127+
9841128// clusterMock creates a cluster to be used for (self) testing.
9851129func (f * clusterFactory ) clusterMock (cfg clusterConfig ) * roachprodCluster {
9861130 return & roachprodCluster {
@@ -1075,6 +1219,7 @@ func (f *clusterFactory) newRoachprodCluster(
10751219 // name. To keep things simple, disable retries in that case.
10761220 maxAttempts = 1
10771221 }
1222+ retryPlanner := newCreateRetryPlanner (cfg .spec , params , selectedArch )
10781223 // loop assumes maxAttempts is atleast (1).
10791224 for i := 1 ; ; i ++ {
10801225 // NB: this intentionally avoids re-using the name across iterations in
@@ -1083,9 +1228,13 @@ func (f *clusterFactory) newRoachprodCluster(
10831228 // https://github.com/cockroachdb/cockroach/issues/67906#issuecomment-887477675
10841229 genName := f .genName (cfg )
10851230
1086- // Set the zones used for the cluster. We call this in the loop as the default GCE zone
1087- // is randomized to avoid zone exhaustion errors.
1088- providerOpts , workloadProviderOpts = cfg .spec .SetRoachprodOptsZones (providerOpts , workloadProviderOpts , params , string (selectedArch ))
1231+ // Set the zones used for this cluster create attempt. The planner keeps
1232+ // normal default-zone selection and capacity-error retry overrides in one
1233+ // place.
1234+ createZones := retryPlanner .zonesForAttempt ()
1235+ providerOpts , workloadProviderOpts = cfg .spec .SetRoachprodOptsZones (
1236+ providerOpts , workloadProviderOpts , params .Cloud , createZones ,
1237+ )
10891238 if clusterCloud != spec .Local {
10901239 providerOptsContainer .SetProviderOpts (clusterCloud .String (), providerOpts )
10911240 workloadProviderOptsContainer .SetProviderOpts (clusterCloud .String (), workloadProviderOpts )
@@ -1149,6 +1298,9 @@ func (f *clusterFactory) newRoachprodCluster(
11491298 }
11501299
11511300 clusterL .PrintfCtx (ctx , "cluster creation failed, cleaning up in case it was partially created: %s" , err )
1301+ if i < maxAttempts {
1302+ retryPlanner .recordFailure (err , clusterL )
1303+ }
11521304 c .Destroy (ctx , closeLogger , clusterL )
11531305 if i >= maxAttempts {
11541306 return nil , nil , err
0 commit comments