Repository: bacalhau-project/bacalhau (language: python)

Issue:
Escape {} to avoid complaints from mdx parser
Using { and } in docs causes problems with the documentation system unless they are escaped. An attempt is made to parse the content as MDX but in likelihood we might be documenting JSON or a Go struct


Discussion / hints:

Fix patch:
diff --git a/pkg/orchestrator/selection/ranking/max_usage.go b/pkg/orchestrator/selection/ranking/max_usage.go
index 7c7084a5c0..c0a0e5ae91 100644
--- a/pkg/orchestrator/selection/ranking/max_usage.go
+++ b/pkg/orchestrator/selection/ranking/max_usage.go
@@ -3,9 +3,11 @@ package ranking
 import (
 	"context"
 	"fmt"
+	"strings"
 
 	"github.com/bacalhau-project/bacalhau/pkg/models"
 	"github.com/bacalhau-project/bacalhau/pkg/orchestrator"
+	"github.com/dustin/go-humanize"
 	"github.com/rs/zerolog/log"
 )
 
@@ -36,11 +38,7 @@ func (s *MaxUsageNodeRanker) RankNodes(ctx context.Context, job models.Job, node
 				reason = "job requires less resources than are available"
 			} else {
 				rank = orchestrator.RankUnsuitable
-				reason = fmt.Sprintf(
-					"job requires more resources %s than are available per job %s",
-					jobResourceUsage.String(),
-					node.ComputeNodeInfo.MaxJobRequirements.String(),
-				)
+				reason = s.formatReason(*jobResourceUsage, node.ComputeNodeInfo.MaxJobRequirements)
 			}
 		}
 		ranks[i] = orchestrator.NodeRank{
@@ -52,3 +50,34 @@ func (s *MaxUsageNodeRanker) RankNodes(ctx context.Context, job models.Job, node
 	}
 	return ranks, nil
 }
+
+const perResourceReason = "more %s (%s) than the maximum available (%s)"
+
+func (s *MaxUsageNodeRanker) formatReason(requested, maximum models.Resources) string {
+	reasons := make([]string, 0, 4) //nolint:gomnd  // number of resources
+	if requested.CPU > maximum.CPU {
+		reasons = append(reasons, fmt.Sprintf(perResourceReason, "CPU",
+			fmt.Sprint(requested.CPU),
+			fmt.Sprint(maximum.CPU),
+		))
+	}
+	if requested.Memory > maximum.Memory {
+		reasons = append(reasons, fmt.Sprintf(perResourceReason, "memory",
+			humanize.Bytes(requested.Memory),
+			humanize.Bytes(maximum.Memory),
+		))
+	}
+	if requested.Disk > maximum.Disk {
+		reasons = append(reasons, fmt.Sprintf(perResourceReason, "disk",
+			humanize.Bytes(requested.Disk),
+			humanize.Bytes(maximum.Disk),
+		))
+	}
+	if requested.GPU > maximum.GPU {
+		reasons = append(reasons, fmt.Sprintf(perResourceReason, "GPUs",
+			fmt.Sprint(requested.GPU),
+			fmt.Sprint(maximum.GPU),
+		))
+	}
+	return fmt.Sprintf("job requires %s", strings.Join(reasons, " and "))
+}


Test patch:
diff --git a/pkg/orchestrator/selection/ranking/max_usage_test.go b/pkg/orchestrator/selection/ranking/max_usage_test.go
index 11df116190..26427301e2 100644
--- a/pkg/orchestrator/selection/ranking/max_usage_test.go
+++ b/pkg/orchestrator/selection/ranking/max_usage_test.go
@@ -22,15 +22,15 @@ type MaxUsageNodeRankerSuite struct {
 func (s *MaxUsageNodeRankerSuite) SetupSuite() {
 	s.smallPeer = models.NodeInfo{
 		NodeID:          "small",
-		ComputeNodeInfo: &models.ComputeNodeInfo{MaxJobRequirements: models.Resources{CPU: 1}},
+		ComputeNodeInfo: &models.ComputeNodeInfo{MaxJobRequirements: models.Resources{CPU: 1, Memory: 1024}},
 	}
 	s.medPeer = models.NodeInfo{
 		NodeID:          "med",
-		ComputeNodeInfo: &models.ComputeNodeInfo{MaxJobRequirements: models.Resources{CPU: 2}},
+		ComputeNodeInfo: &models.ComputeNodeInfo{MaxJobRequirements: models.Resources{CPU: 2, Memory: 1024e3}},
 	}
 	s.largePeer = models.NodeInfo{
 		NodeID:          "large",
-		ComputeNodeInfo: &models.ComputeNodeInfo{MaxJobRequirements: models.Resources{CPU: 3}},
+		ComputeNodeInfo: &models.ComputeNodeInfo{MaxJobRequirements: models.Resources{CPU: 3, Memory: 1024e6}},
 	}
 }
 
@@ -73,7 +73,7 @@ func (s *MaxUsageNodeRankerSuite) TestRankNodes_MedJob() {
 	ranks, err := s.MaxUsageNodeRanker.RankNodes(context.Background(), *job, nodes)
 	s.NoError(err)
 	s.Equal(len(nodes), len(ranks))
-	assertEquals(s.T(), ranks, "small", -1)
+	assertEquals(s.T(), ranks, "small", -1, "job requires more CPU (2) than the maximum available (1)")
 	assertEquals(s.T(), ranks, "med", 10)
 	assertEquals(s.T(), ranks, "large", 10)
 }
@@ -85,8 +85,8 @@ func (s *MaxUsageNodeRankerSuite) TestRankNodes_LargeJob() {
 	ranks, err := s.MaxUsageNodeRanker.RankNodes(context.Background(), *job, nodes)
 	s.NoError(err)
 	s.Equal(len(nodes), len(ranks))
-	assertEquals(s.T(), ranks, "small", -1)
-	assertEquals(s.T(), ranks, "med", -1)
+	assertEquals(s.T(), ranks, "small", -1, "job requires more CPU (3) than the maximum available (1)")
+	assertEquals(s.T(), ranks, "med", -1, "job requires more CPU (3) than the maximum available (2)")
 	assertEquals(s.T(), ranks, "large", 10)
 }
 
@@ -98,9 +98,35 @@ func (s *MaxUsageNodeRankerSuite) TestRankNodes_VeryLargeJob() {
 	ranks, err := s.MaxUsageNodeRanker.RankNodes(context.Background(), *job, nodes)
 	s.NoError(err)
 	s.Equal(len(nodes), len(ranks))
-	assertEquals(s.T(), ranks, "small", -1)
-	assertEquals(s.T(), ranks, "med", -1)
-	assertEquals(s.T(), ranks, "large", -1)
+	assertEquals(s.T(), ranks, "small", -1, "job requires more CPU (3.1) than the maximum available (1)")
+	assertEquals(s.T(), ranks, "med", -1, "job requires more CPU (3.1) than the maximum available (2)")
+	assertEquals(s.T(), ranks, "large", -1, "job requires more CPU (3.1) than the maximum available (3)")
+}
+
+func (s *MaxUsageNodeRankerSuite) TestRankNodes_MemoryReporting() {
+	job := mock.Job()
+	job.Task().ResourcesConfig = &models.ResourcesConfig{Memory: "2GB"}
+
+	nodes := []models.NodeInfo{s.smallPeer, s.medPeer, s.largePeer}
+	ranks, err := s.MaxUsageNodeRanker.RankNodes(context.Background(), *job, nodes)
+	s.NoError(err)
+	s.Equal(len(nodes), len(ranks))
+	assertEquals(s.T(), ranks, "small", -1, "job requires more memory (2.0 GB) than the maximum available (1.0 kB)")
+	assertEquals(s.T(), ranks, "med", -1, "job requires more memory (2.0 GB) than the maximum available (1.0 MB)")
+	assertEquals(s.T(), ranks, "large", -1, "job requires more memory (2.0 GB) than the maximum available (1.0 GB)")
+}
+
+func (s *MaxUsageNodeRankerSuite) TestRankNodes_MultpleReporting() {
+	job := mock.Job()
+	job.Task().ResourcesConfig = &models.ResourcesConfig{CPU: "3", Memory: "2GB"}
+
+	nodes := []models.NodeInfo{s.smallPeer, s.medPeer, s.largePeer}
+	ranks, err := s.MaxUsageNodeRanker.RankNodes(context.Background(), *job, nodes)
+	s.NoError(err)
+	s.Equal(len(nodes), len(ranks))
+	assertEquals(s.T(), ranks, "small", -1, "job requires more CPU (3) than the maximum available (1) and more memory (2.0 GB) than the maximum available (1.0 kB)")
+	assertEquals(s.T(), ranks, "med", -1, "job requires more CPU (3) than the maximum available (2) and more memory (2.0 GB) than the maximum available (1.0 MB)")
+	assertEquals(s.T(), ranks, "large", -1, "job requires more memory (2.0 GB) than the maximum available (1.0 GB)")
 }
 
 func (s *MaxUsageNodeRankerSuite) TestRankNodesUnknownJob() {
diff --git a/pkg/orchestrator/selection/ranking/utils_test.go b/pkg/orchestrator/selection/ranking/utils_test.go
index bb6951c12a..08ebc6faf0 100644
--- a/pkg/orchestrator/selection/ranking/utils_test.go
+++ b/pkg/orchestrator/selection/ranking/utils_test.go
@@ -1,17 +1,21 @@
 package ranking
 
 import (
+	"slices"
 	"testing"
 
 	"github.com/bacalhau-project/bacalhau/pkg/orchestrator"
 )
 
-func assertEquals(t *testing.T, ranks []orchestrator.NodeRank, nodeID string, expectedRank int) {
+func assertEquals(t *testing.T, ranks []orchestrator.NodeRank, nodeID string, expectedRank int, expectedReason ...string) {
 	for _, rank := range ranks {
 		if rank.NodeInfo.ID() == nodeID {
 			if rank.Rank != expectedRank {
 				t.Errorf("expected rank %d for node %s, got %d", expectedRank, nodeID, rank.Rank)
 			}
+			if len(expectedReason) > 0 && !slices.Contains(expectedReason, rank.Reason) {
+				t.Errorf("expected reason %q for node %s, got %q", expectedReason[0], nodeID, rank.Reason)
+			}
 			return
 		}
 	}