From c06dea13bfe86360808944f07798cf8147fbad33 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Mon, 5 Feb 2024 13:47:31 +0100 Subject: [PATCH 1/2] autopilot: adjust min score if fewer candidates than needed are available --- autopilot/contractor.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/autopilot/contractor.go b/autopilot/contractor.go index b4a0c1cc3..2af724f4d 100644 --- a/autopilot/contractor.go +++ b/autopilot/contractor.go @@ -1197,11 +1197,17 @@ func (c *contractor) calculateMinScore(ctx context.Context, candidates []scoredH return math.SmallestNonzeroFloat64, nil } + // determine the number of random hosts we fetch per iteration when + // calculating the min score - it contains a constant factor in case the + // number of contracts is very low and a linear factor to make sure the + // number is relative to the number of contracts we want to form + randSetSize := 2*int(numContracts) + 50 + // do multiple rounds to select the lowest score var lowestScores []float64 for r := 0; r < 5; r++ { lowestScore := math.MaxFloat64 - for _, host := range scoredHosts(candidates).randSelectByScore(int(numContracts) + 50) { // buffer + for _, host := range scoredHosts(candidates).randSelectByScore(randSetSize) { if host.score < lowestScore { lowestScore = host.score } @@ -1216,8 +1222,17 @@ func (c *contractor) calculateMinScore(ctx context.Context, candidates []scoredH } minScore := lowestScore / minAllowedScoreLeeway + // make sure the min score allows for 'numContracts' contracts to be formed + if len(candidates) < int(numContracts) { + return math.SmallestNonzeroFloat64, nil + } else if cutoff := candidates[numContracts-1].score; minScore < cutoff { + minScore = cutoff + } + c.logger.Infow("finished computing minScore", + "candidates", len(candidates), "minScore", minScore, + "numContracts", numContracts, "lowestScore", lowestScore) return minScore, nil } From ab95e07317ad33985225b79498af38aa0ae1f611 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Mon, 5 Feb 2024 14:15:09 +0100 Subject: [PATCH 2/2] autopilot: add TestCalculateMinScore --- autopilot/contractor.go | 21 ++++++++++---------- autopilot/contractor_test.go | 38 ++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 autopilot/contractor_test.go diff --git a/autopilot/contractor.go b/autopilot/contractor.go index 2af724f4d..adad5d1b7 100644 --- a/autopilot/contractor.go +++ b/autopilot/contractor.go @@ -246,10 +246,7 @@ func (c *contractor) performContractMaintenance(ctx context.Context, w Worker) ( // min score to pass checks var minScore float64 if len(hosts) > 0 { - minScore, err = c.calculateMinScore(ctx, candidates, state.cfg.Contracts.Amount) - if err != nil { - return false, fmt.Errorf("failed to determine min score for contract check: %w", err) - } + minScore = c.calculateMinScore(ctx, candidates, state.cfg.Contracts.Amount) } else { c.logger.Warn("could not calculate min score, no hosts found") } @@ -1190,11 +1187,11 @@ func (c *contractor) renewFundingEstimate(ctx context.Context, ci contractInfo, return cappedEstimatedCost, nil } -func (c *contractor) calculateMinScore(ctx context.Context, candidates []scoredHost, numContracts uint64) (float64, error) { +func (c *contractor) calculateMinScore(ctx context.Context, candidates []scoredHost, numContracts uint64) float64 { // return early if there's no hosts if len(candidates) == 0 { c.logger.Warn("min host score is set to the smallest non-zero float because there are no candidate hosts") - return math.SmallestNonzeroFloat64, nil + return math.SmallestNonzeroFloat64 } // determine the number of random hosts we fetch per iteration when @@ -1216,16 +1213,20 @@ func (c *contractor) calculateMinScore(ctx context.Context, candidates []scoredH } // compute the min score + var lowestScore float64 lowestScore, err := stats.Float64Data(lowestScores).Median() if err != nil { - return 0, err + panic("never fails since len(candidates) > 0 so len(lowestScores) > 0 as well") } minScore := lowestScore / minAllowedScoreLeeway // make sure the min score allows for 'numContracts' contracts to be formed + sort.Slice(candidates, func(i, j int) bool { + return candidates[i].score > candidates[j].score + }) if len(candidates) < int(numContracts) { - return math.SmallestNonzeroFloat64, nil - } else if cutoff := candidates[numContracts-1].score; minScore < cutoff { + return math.SmallestNonzeroFloat64 + } else if cutoff := candidates[numContracts-1].score; minScore > cutoff { minScore = cutoff } @@ -1234,7 +1235,7 @@ func (c *contractor) calculateMinScore(ctx context.Context, candidates []scoredH "minScore", minScore, "numContracts", numContracts, "lowestScore", lowestScore) - return minScore, nil + return minScore } func (c *contractor) candidateHosts(ctx context.Context, hosts []hostdb.Host, usedHosts map[types.PublicKey]struct{}, storedData map[types.PublicKey]uint64, minScore float64) ([]scoredHost, unusableHostResult, error) { diff --git a/autopilot/contractor_test.go b/autopilot/contractor_test.go new file mode 100644 index 000000000..a0f63425b --- /dev/null +++ b/autopilot/contractor_test.go @@ -0,0 +1,38 @@ +package autopilot + +import ( + "context" + "math" + "testing" + + "go.uber.org/zap" +) + +func TestCalculateMinScore(t *testing.T) { + c := &contractor{ + logger: zap.NewNop().Sugar(), + } + + var candidates []scoredHost + for i := 0; i < 250; i++ { + candidates = append(candidates, scoredHost{score: float64(i + 1)}) + } + + // Test with 100 hosts which makes for a random set size of 250 + minScore := c.calculateMinScore(context.Background(), candidates, 100) + if minScore != 0.002 { + t.Fatalf("expected minScore to be 0.002 but was %v", minScore) + } + + // Test with 0 hosts + minScore = c.calculateMinScore(context.Background(), []scoredHost{}, 100) + if minScore != math.SmallestNonzeroFloat64 { + t.Fatalf("expected minScore to be math.SmallestNonzeroFLoat64 but was %v", minScore) + } + + // Test with 300 hosts which is 50 more than we have + minScore = c.calculateMinScore(context.Background(), candidates, 300) + if minScore != math.SmallestNonzeroFloat64 { + t.Fatalf("expected minScore to be math.SmallestNonzeroFLoat64 but was %v", minScore) + } +}