diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index f3358bcc5..43aa0b4f3 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -15,7 +15,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - GO_VERSION: [ "1.19","1.20","1.21" ] + GO_VERSION: + - '1.24' # named in go.mod + - 'oldstable' + - 'stable' steps: - name: "Fetch source code" uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 @@ -49,7 +52,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - GO_VERSION: [ "1.19","1.20","1.21" ] + GO_VERSION: + - '1.24' # named in go.mod + - 'oldstable' + - 'stable' steps: - name: "Fetch source code" uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 @@ -90,7 +96,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - GO_VERSION: [ "1.19","1.20","1.21" ] + GO_VERSION: + - '1.24' # named in go.mod + - 'oldstable' + - 'stable' steps: - name: "Fetch source code" uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 @@ -122,7 +131,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - GO_VERSION: [ "1.19","1.20","1.21" ] + GO_VERSION: + - '1.24' # named in go.mod + - 'oldstable' + - 'stable' steps: - name: "Fetch source code" uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 diff --git a/coordinate/performance_test.go b/coordinate/performance_test.go index 44201b2a7..be54a8b7f 100644 --- a/coordinate/performance_test.go +++ b/coordinate/performance_test.go @@ -7,179 +7,199 @@ import ( "math" "testing" "time" + + "github.com/hashicorp/serf/testutil/retry" ) func TestPerformance_Line(t *testing.T) { - const spacing = 10 * time.Millisecond - const nodes, cycles = 10, 1000 - config := DefaultConfig() - clients, err := GenerateClients(nodes, config) - if err != nil { - t.Fatal(err) - } - truth := GenerateLine(nodes, spacing) - Simulate(clients, truth, cycles) - stats := Evaluate(clients, truth) - if stats.ErrorAvg > 0.0018 || stats.ErrorMax > 0.0092 { - t.Fatalf("performance stats are out of spec: %v", stats) - } + retry.RunWith(retry.RunNTimesWithWait(20, 25), t, func(r *retry.R) { + + const spacing = 10 * time.Millisecond + const nodes, cycles = 10, 1000 + config := DefaultConfig() + clients, err := GenerateClients(nodes, config) + if err != nil { + r.Fatal(err) + } + truth := GenerateLine(nodes, spacing) + Simulate(clients, truth, cycles) + stats := Evaluate(clients, truth) + if stats.ErrorAvg > 0.0018 || stats.ErrorMax > 0.0092 { + r.Fatalf("performance stats are out of spec: %v", stats) + } + }) } func TestPerformance_Grid(t *testing.T) { - const spacing = 10 * time.Millisecond - const nodes, cycles = 25, 1000 - config := DefaultConfig() - clients, err := GenerateClients(nodes, config) - if err != nil { - t.Fatal(err) - } - truth := GenerateGrid(nodes, spacing) - Simulate(clients, truth, cycles) - stats := Evaluate(clients, truth) - if stats.ErrorAvg > 0.0015 || stats.ErrorMax > 0.022 { - t.Fatalf("performance stats are out of spec: %v", stats) - } + retry.RunWith(retry.RunNTimesWithWait(20, 25), t, func(r *retry.R) { + + const spacing = 10 * time.Millisecond + const nodes, cycles = 25, 1000 + config := DefaultConfig() + clients, err := GenerateClients(nodes, config) + if err != nil { + r.Fatal(err) + } + truth := GenerateGrid(nodes, spacing) + Simulate(clients, truth, cycles) + stats := Evaluate(clients, truth) + if stats.ErrorAvg > 0.0015 || stats.ErrorMax > 0.022 { + r.Fatalf("performance stats are out of spec: %v", stats) + } + }) } func TestPerformance_Split(t *testing.T) { - const lan, wan = 1 * time.Millisecond, 10 * time.Millisecond - const nodes, cycles = 25, 1000 - config := DefaultConfig() - clients, err := GenerateClients(nodes, config) - if err != nil { - t.Fatal(err) - } - truth := GenerateSplit(nodes, lan, wan) - Simulate(clients, truth, cycles) - stats := Evaluate(clients, truth) - if stats.ErrorAvg > 0.000060 || stats.ErrorMax > 0.00048 { - t.Fatalf("performance stats are out of spec: %v", stats) - } + retry.RunWith(retry.RunNTimesWithWait(20, 25), t, func(r *retry.R) { + + const lan, wan = 1 * time.Millisecond, 10 * time.Millisecond + const nodes, cycles = 25, 1000 + config := DefaultConfig() + clients, err := GenerateClients(nodes, config) + if err != nil { + r.Fatal(err) + } + truth := GenerateSplit(nodes, lan, wan) + Simulate(clients, truth, cycles) + stats := Evaluate(clients, truth) + if stats.ErrorAvg > 0.000060 || stats.ErrorMax > 0.00048 { + r.Fatalf("performance stats are out of spec: %v", stats) + } + }) } func TestPerformance_Height(t *testing.T) { - const radius = 100 * time.Millisecond - const nodes, cycles = 25, 1000 - - // Constrain us to two dimensions so that we can just exactly represent - // the circle. - config := DefaultConfig() - config.Dimensionality = 2 - clients, err := GenerateClients(nodes, config) - if err != nil { - t.Fatal(err) - } - - // Generate truth where the first coordinate is in the "middle" because - // it's equidistant from all the nodes, but it will have an extra radius - // added to the distance, so it should come out above all the others. - truth := GenerateCircle(nodes, radius) - Simulate(clients, truth, cycles) - - // Make sure the height looks reasonable with the regular nodes all in a - // plane, and the center node up above. - for i := range clients { - coord := clients[i].GetCoordinate() - if i == 0 { - if coord.Height < 0.97*radius.Seconds() { - t.Fatalf("height is out of spec: %9.6f", coord.Height) - } - } else { - if coord.Height > 0.03*radius.Seconds() { - t.Fatalf("height is out of spec: %9.6f", coord.Height) + retry.RunWith(retry.RunNTimesWithWait(20, 25), t, func(r *retry.R) { + + const radius = 100 * time.Millisecond + const nodes, cycles = 25, 1000 + + // Constrain us to two dimensions so that we can just exactly represent + // the circle. + config := DefaultConfig() + config.Dimensionality = 2 + clients, err := GenerateClients(nodes, config) + if err != nil { + r.Fatal(err) + } + + // Generate truth where the first coordinate is in the "middle" because + // it's equidistant from all the nodes, but it will have an extra radius + // added to the distance, so it should come out above all the others. + truth := GenerateCircle(nodes, radius) + Simulate(clients, truth, cycles) + + // Make sure the height looks reasonable with the regular nodes all in a + // plane, and the center node up above. + for i := range clients { + coord := clients[i].GetCoordinate() + if i == 0 { + if coord.Height < 0.97*radius.Seconds() { + r.Fatalf("height is out of spec: %9.6f", coord.Height) + } + } else { + if coord.Height > 0.03*radius.Seconds() { + r.Fatalf("height is out of spec: %9.6f", coord.Height) + } } } - } - stats := Evaluate(clients, truth) - if stats.ErrorAvg > 0.0025 || stats.ErrorMax > 0.064 { - t.Fatalf("performance stats are out of spec: %v", stats) - } + stats := Evaluate(clients, truth) + if stats.ErrorAvg > 0.0025 || stats.ErrorMax > 0.064 { + r.Fatalf("performance stats are out of spec: %v", stats) + } + }) } func TestPerformance_Drift(t *testing.T) { - const dist = 500 * time.Millisecond - const nodes = 4 - config := DefaultConfig() - config.Dimensionality = 2 - clients, err := GenerateClients(nodes, config) - if err != nil { - t.Fatal(err) - } - - // Do some icky surgery on the clients to put them into a square, up in - // the first quadrant. - clients[0].coord.Vec = []float64{0.0, 0.0} - clients[1].coord.Vec = []float64{0.0, dist.Seconds()} - clients[2].coord.Vec = []float64{dist.Seconds(), dist.Seconds()} - clients[3].coord.Vec = []float64{dist.Seconds(), dist.Seconds()} - - // Make a corresponding truth matrix. The nodes are laid out like this - // so the distances are all equal, except for the diagonal: - // - // (1) <- dist -> (2) - // - // | <- dist | - // | | - // | dist -> | - // - // (0) <- dist -> (3) - // - truth := make([][]time.Duration, nodes) - for i := range truth { - truth[i] = make([]time.Duration, nodes) - } - for i := 0; i < nodes; i++ { - for j := i + 1; j < nodes; j++ { - rtt := dist - if (i%2 == 0) && (j%2 == 0) { - rtt = time.Duration(math.Sqrt2 * float64(rtt)) - } - truth[i][j], truth[j][i] = rtt, rtt + retry.RunWith(retry.RunNTimesWithWait(20, 25), t, func(r *retry.R) { + + const dist = 500 * time.Millisecond + const nodes = 4 + config := DefaultConfig() + config.Dimensionality = 2 + clients, err := GenerateClients(nodes, config) + if err != nil { + r.Fatal(err) } - } - calcCenterError := func() float64 { - min, max := clients[0].GetCoordinate(), clients[0].GetCoordinate() - for i := 1; i < nodes; i++ { - coord := clients[i].GetCoordinate() - for j, v := range coord.Vec { - min.Vec[j] = math.Min(min.Vec[j], v) - max.Vec[j] = math.Max(max.Vec[j], v) + // Do some icky surgery on the clients to put them into a square, up in + // the first quadrant. + clients[0].coord.Vec = []float64{0.0, 0.0} + clients[1].coord.Vec = []float64{0.0, dist.Seconds()} + clients[2].coord.Vec = []float64{dist.Seconds(), dist.Seconds()} + clients[3].coord.Vec = []float64{dist.Seconds(), dist.Seconds()} + + // Make a corresponding truth matrix. The nodes are laid out like this + // so the distances are all equal, except for the diagonal: + // + // (1) <- dist -> (2) + // + // | <- dist | + // | | + // | dist -> | + // + // (0) <- dist -> (3) + // + truth := make([][]time.Duration, nodes) + for i := range truth { + truth[i] = make([]time.Duration, nodes) + } + for i := 0; i < nodes; i++ { + for j := i + 1; j < nodes; j++ { + rtt := dist + if (i%2 == 0) && (j%2 == 0) { + rtt = time.Duration(math.Sqrt2 * float64(rtt)) + } + truth[i][j], truth[j][i] = rtt, rtt } } - mid := make([]float64, config.Dimensionality) - for i := range mid { - mid[i] = min.Vec[i] + (max.Vec[i]-min.Vec[i])/2 + calcCenterError := func() float64 { + min, max := clients[0].GetCoordinate(), clients[0].GetCoordinate() + for i := 1; i < nodes; i++ { + coord := clients[i].GetCoordinate() + for j, v := range coord.Vec { + min.Vec[j] = math.Min(min.Vec[j], v) + max.Vec[j] = math.Max(max.Vec[j], v) + } + } + + mid := make([]float64, config.Dimensionality) + for i := range mid { + mid[i] = min.Vec[i] + (max.Vec[i]-min.Vec[i])/2 + } + return magnitude(mid) } - return magnitude(mid) - } - // Let the simulation run for a while to stabilize, then snap a baseline - // for the center error. - Simulate(clients, truth, 1000) - baseline := calcCenterError() + // Let the simulation run for a while to stabilize, then snap a baseline + // for the center error. + Simulate(clients, truth, 1000) + baseline := calcCenterError() - // Now run for a bunch more cycles and see if gravity pulls the center - // in the right direction. - Simulate(clients, truth, 10000) - if error := calcCenterError(); error > 0.8*baseline { - t.Fatalf("drift performance out of spec: %9.6f -> %9.6f", baseline, error) - } + // Now run for a bunch more cycles and see if gravity pulls the center + // in the right direction. + Simulate(clients, truth, 10000) + if error := calcCenterError(); error > 0.8*baseline { + r.Fatalf("drift performance out of spec: %9.6f -> %9.6f", baseline, error) + } + }) } func TestPerformance_Random(t *testing.T) { - const mean, deviation = 100 * time.Millisecond, 10 * time.Millisecond - const nodes, cycles = 25, 1000 - config := DefaultConfig() - clients, err := GenerateClients(nodes, config) - if err != nil { - t.Fatal(err) - } - truth := GenerateRandom(nodes, mean, deviation) - Simulate(clients, truth, cycles) - stats := Evaluate(clients, truth) - if stats.ErrorAvg > 0.075 || stats.ErrorMax > 0.33 { - t.Fatalf("performance stats are out of spec: %v", stats) - } + retry.RunWith(retry.RunNTimesWithWait(20, 25), t, func(r *retry.R) { + + const mean, deviation = 100 * time.Millisecond, 10 * time.Millisecond + const nodes, cycles = 25, 1000 + config := DefaultConfig() + clients, err := GenerateClients(nodes, config) + if err != nil { + r.Fatal(err) + } + truth := GenerateRandom(nodes, mean, deviation) + Simulate(clients, truth, cycles) + stats := Evaluate(clients, truth) + if stats.ErrorAvg > 0.075 || stats.ErrorMax > 0.33 { + r.Fatalf("performance stats are out of spec: %v", stats) + } + }) } diff --git a/go.mod b/go.mod index 8dcb22bb8..71408972b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/hashicorp/serf -go 1.19 +go 1.24.0 require ( github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e diff --git a/go.sum b/go.sum index ec8045bcc..b1bf06354 100644 --- a/go.sum +++ b/go.sum @@ -210,6 +210,7 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -257,5 +258,6 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/testutil/retry/retry.go b/testutil/retry/retry.go index 2914b6ade..c86519c7d 100644 --- a/testutil/retry/retry.go +++ b/testutil/retry/retry.go @@ -155,6 +155,10 @@ func ThreeTimes() *Counter { return &Counter{Count: 3, Wait: 25 * time.Millisecond} } +func RunNTimesWithWait(n int, wait int) *Counter { + return &Counter{Count: n, Wait: time.Duration(wait) * time.Millisecond} +} + // Retryer provides an interface for repeating operations // until they succeed or an exit condition is met. type Retryer interface {