Repository: etcd-io/etcd
File: tests/framework/e2e/downgrade.go (Go)

Code before:
		ValidateVersion(t, epc.Cfg, epc.Procs[i], version.Versions{
			Cluster: ver.String(),
			Server:  OffsetMinor(ver, 1).String(),
			Storage: ver.String(),
		})
		AssertProcessLogs(t, epc.Procs[i], "The server is ready to downgrade")
	}

	t.Log("Cluster is ready for downgrade")
}

func DowngradeCancel(t *testing.T, epc *EtcdProcessCluster) {
	t.Logf("etcdctl downgrade cancel")
	c := epc.Etcdctl()

	// Due to the fact that cancellation request can timeout quite easily, we retry it for a bounded number of times before failing the test to reduce flakiness
	var err error
	testutils.ExecuteWithTimeout(t, 2*time.Minute, func() {
		maxRetryCount := 5
		for retryCount := 1; retryCount <= maxRetryCount; retryCount++ {
			t.Logf("etcdctl downgrade cancel (attempt %v)", retryCount)
			err = c.DowngradeCancel(context.TODO())
			if err != nil {
				if strings.Contains(err.Error(), "context deadline exceeded") {
					t.Logf("etcdctl downgrade cancel context deadline exceeded, retrying again (attempt %v)", retryCount)
					continue
				} else if strings.Contains(err.Error(), "request timed out") {
					t.Logf("etcdctl downgrade cancel request timed out, retrying again (attempt %v)", retryCount)
					continue
				}

				if retryCount > 1 && strings.Contains(err.Error(), "no inflight downgrade job") {
					// if the cancellation request times out, the cancellation can still be performed successfully
					t.Logf("etcdctl downgrade cancel executed successfully though timed out in the previous call (attempt %v)", retryCount)
					err = nil
					break
				}
			}

			t.Logf("etcdctl downgrade cancel executed successfully without timing out (attempt %v)", retryCount)
			break
		}
	})

	require.NoError(t, err)

	t.Log("Cluster downgrade cancellation is completed")
}

func DowngradeUpgradeMembers(t *testing.T, lg *zap.Logger, clus *EtcdProcessCluster, numberOfMembersToChange int, currentVersion, targetVersion *semver.Version) error {
	membersToChange := rand.Perm(len(clus.Procs))[:numberOfMembersToChange]

Review comment (suggestion):
```
if strings.Contains(err.Error(), "no inflight downgrade job") {
	// no need for cancel
	t.Log(err)
	err = nil
	break
}
t.Logf("etcdctl downgrade error: %w, retrying again (attempt %v)", err, retryCount)
```

Code after:
		ValidateVersion(t, epc.Cfg, epc.Procs[i], version.Versions{
			Cluster: ver.String(),
			Server:  OffsetMinor(ver, 1).String(),
			Storage: ver.String(),
		})
		AssertProcessLogs(t, epc.Procs[i], "The server is ready to downgrade")
	}

	t.Log("Cluster is ready for downgrade")
}

func DowngradeCancel(t *testing.T, epc *EtcdProcessCluster) {
	c := epc.Etcdctl()

	var err error
	testutils.ExecuteWithTimeout(t, 1*time.Minute, func() {
		for {
			t.Logf("etcdctl downgrade cancel")
			err = c.DowngradeCancel(context.TODO())
			if err != nil {
				if strings.Contains(err.Error(), "no inflight downgrade job") {
					// cancellation has been performed successfully
					t.Log(err)
					err = nil
					break
				}

				t.Logf("etcdctl downgrade error: %v, retrying", err)
				continue
			}

			t.Logf("etcdctl downgrade cancel executed successfully")
			break
		}
	})

	require.NoError(t, err)

	t.Log("Cluster downgrade cancellation is completed")
}

func DowngradeUpgradeMembers(t *testing.T, lg *zap.Logger, clus *EtcdProcessCluster, numberOfMembersToChange int, currentVersion, targetVersion *semver.Version) error {
	membersToChange := rand.Perm(len(clus.Procs))[:numberOfMembersToChange]
	t.Logf(fmt.Sprintln("Elect members for operations"), zap.Any("members", membersToChange))

	return DowngradeUpgradeMembersByID(t, lg, clus, membersToChange, currentVersion, targetVersion)
}

func DowngradeUpgradeMembersByID(t *testing.T, lg *zap.Logger, clus *EtcdProcessCluster, membersToChange []int, currentVersion, targetVersion *semver.Version) error {
	if lg == nil {
		lg = clus.lg