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

Issue:
More architectural placement issues
```
❯ docker run ubuntu echo "hello world"
hello world
❯ ./bin/darwin/arm64/bacalhau docker run ubuntu echo "hello world"
Using default tag: latest. Please specify a tag/digest for better reproducibility.
Job successfully submitted. Job ID: c1e7d172-3cb7-4cbb-824f-851346097bad
Checking job status... (Enter Ctrl+C to exit at any time, your job will continue running):

	Communicating with the network  ................  done ✅  0.0s
	   Creating job for submission  ................  err  ❌  0.5s

Error submitting job: not enough nodes to run job. requested: 1, available: 0.
Job Results By Node:

To get more details about the run, execute:
	bacalhau describe c1e7d172-3cb7-4cbb-824f-851346097bad

╭─   ~/code/bacalhau   fixing_ui_errors !1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
╰─❯

```


Discussion / hints:
```
→ bacalhau describe c1e7d172-3cb7-4cbb-824f-851346097bad

Job not found. ID: c1e7d172-3cb7-4cbb-824f-851346097bad
```
Looks like the job is sent to a local network and not to the demo network. Do you have docker installed? What is the output of `bacalhau node list --output yaml`?

Fix patch:
diff --git a/pkg/orchestrator/interfaces.go b/pkg/orchestrator/interfaces.go
index 07377481b5..b75032a0df 100644
--- a/pkg/orchestrator/interfaces.go
+++ b/pkg/orchestrator/interfaces.go
@@ -91,7 +91,6 @@ type Planner interface {
 // NodeDiscoverer discovers nodes in the network that are suitable to execute a job.
 type NodeDiscoverer interface {
 	ListNodes(ctx context.Context) ([]models.NodeInfo, error)
-	FindNodes(ctx context.Context, job models.Job) ([]models.NodeInfo, error)
 }
 
 // NodeRanker ranks nodes based on their suitability to execute a job.
diff --git a/pkg/orchestrator/selection/discovery/chained.go b/pkg/orchestrator/selection/discovery/chained.go
index 9c307606fc..a3278cc7a8 100644
--- a/pkg/orchestrator/selection/discovery/chained.go
+++ b/pkg/orchestrator/selection/discovery/chained.go
@@ -27,12 +27,6 @@ func (c *Chain) Add(discoverer ...orchestrator.NodeDiscoverer) {
 	c.discoverers = append(c.discoverers, discoverer...)
 }
 
-func (c *Chain) FindNodes(ctx context.Context, job models.Job) ([]models.NodeInfo, error) {
-	return c.chainDiscovery(ctx, "FindNodes", func(r orchestrator.NodeDiscoverer) ([]models.NodeInfo, error) {
-		return r.FindNodes(ctx, job)
-	})
-}
-
 func (c *Chain) ListNodes(ctx context.Context) ([]models.NodeInfo, error) {
 	return c.chainDiscovery(ctx, "ListNodes", func(r orchestrator.NodeDiscoverer) ([]models.NodeInfo, error) {
 		return r.ListNodes(ctx)
diff --git a/pkg/orchestrator/selection/discovery/store.go b/pkg/orchestrator/selection/discovery/store.go
index 0f3e4e9559..431a5f9868 100644
--- a/pkg/orchestrator/selection/discovery/store.go
+++ b/pkg/orchestrator/selection/discovery/store.go
@@ -22,12 +22,6 @@ func NewStoreNodeDiscoverer(params StoreNodeDiscovererParams) *StoreNodeDiscover
 	}
 }
 
-// FindNodes returns the nodes that support the job's execution engine, and have enough TOTAL capacity to run the job.
-func (d *StoreNodeDiscoverer) FindNodes(ctx context.Context, job models.Job) ([]models.NodeInfo, error) {
-	// filter nodes that support the job's engine
-	return d.store.ListForEngine(ctx, job.Task().Engine.Type)
-}
-
 // ListNodes implements orchestrator.NodeDiscoverer
 func (d *StoreNodeDiscoverer) ListNodes(ctx context.Context) ([]models.NodeInfo, error) {
 	return d.store.List(ctx)
diff --git a/pkg/orchestrator/selection/ranking/features.go b/pkg/orchestrator/selection/ranking/features.go
index f657e9ed3c..af5e4714cb 100644
--- a/pkg/orchestrator/selection/ranking/features.go
+++ b/pkg/orchestrator/selection/ranking/features.go
@@ -74,7 +74,8 @@ func (s *featureNodeRanker) rankNode(ctx context.Context, node models.NodeInfo,
 
 		if !found {
 			// Target wasn't found – we can end early as we won't use this node.
-			return orchestrator.RankUnsuitable, fmt.Sprintf("does not support %s, only %s", requiredKey, providedKeys)
+			availableKeys := strings.Join(providedKeys, ", ")
+			return orchestrator.RankUnsuitable, fmt.Sprintf("does not support %s, only %s", requiredKey, availableKeys)
 		}
 	}
 
diff --git a/pkg/orchestrator/selection/selector/node_selector.go b/pkg/orchestrator/selection/selector/node_selector.go
index d044ef4214..ef7ee238c3 100644
--- a/pkg/orchestrator/selection/selector/node_selector.go
+++ b/pkg/orchestrator/selection/selector/node_selector.go
@@ -62,7 +62,7 @@ func (n NodeSelector) TopMatchingNodes(ctx context.Context, job *models.Job, des
 }
 
 func (n NodeSelector) rankAndFilterNodes(ctx context.Context, job *models.Job) (selected, rejected []orchestrator.NodeRank, err error) {
-	nodeIDs, err := n.nodeDiscoverer.FindNodes(ctx, *job)
+	nodeIDs, err := n.nodeDiscoverer.ListNodes(ctx)
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/pkg/routing/inmemory/inmemory.go b/pkg/routing/inmemory/inmemory.go
index d3743c2999..527775ad24 100644
--- a/pkg/routing/inmemory/inmemory.go
+++ b/pkg/routing/inmemory/inmemory.go
@@ -24,17 +24,15 @@ type NodeInfoStoreParams struct {
 }
 
 type NodeInfoStore struct {
-	ttl             time.Duration
-	nodeInfoMap     map[string]nodeInfoWrapper
-	engineNodeIDMap map[string]map[string]struct{}
-	mu              sync.RWMutex
+	ttl         time.Duration
+	nodeInfoMap map[string]nodeInfoWrapper
+	mu          sync.RWMutex
 }
 
 func NewNodeInfoStore(params NodeInfoStoreParams) *NodeInfoStore {
 	return &NodeInfoStore{
-		ttl:             params.TTL,
-		nodeInfoMap:     make(map[string]nodeInfoWrapper),
-		engineNodeIDMap: make(map[string]map[string]struct{}),
+		ttl:         params.TTL,
+		nodeInfoMap: make(map[string]nodeInfoWrapper),
 	}
 }
 
@@ -42,34 +40,8 @@ func (r *NodeInfoStore) Add(ctx context.Context, nodeInfo models.NodeInfo) error
 	r.mu.Lock()
 	defer r.mu.Unlock()
 
-	// delete node from previous engines if it already exists to replace old engines with new ones if they've changed
-	nodeID := nodeInfo.ID()
-	existingNodeInfo, ok := r.nodeInfoMap[nodeID]
-	if ok {
-		if existingNodeInfo.ComputeNodeInfo != nil {
-			for _, engine := range existingNodeInfo.ComputeNodeInfo.ExecutionEngines {
-				delete(r.engineNodeIDMap[engine], nodeID)
-			}
-		}
-	} else {
-		var engines []string
-		if nodeInfo.ComputeNodeInfo != nil {
-			engines = append(engines, nodeInfo.ComputeNodeInfo.ExecutionEngines...)
-		}
-		log.Ctx(ctx).Debug().Msgf("Adding new node %s to in-memory nodeInfo store with engines %v", nodeID, engines)
-	}
-
-	// TODO: use data structure that maintains nodes in descending order based on available capacity.
-	if nodeInfo.ComputeNodeInfo != nil {
-		for _, engine := range nodeInfo.ComputeNodeInfo.ExecutionEngines {
-			if _, ok := r.engineNodeIDMap[engine]; !ok {
-				r.engineNodeIDMap[engine] = make(map[string]struct{})
-			}
-			r.engineNodeIDMap[engine][nodeID] = struct{}{}
-		}
-	}
-
 	// add or update the node info
+	nodeID := nodeInfo.ID()
 	r.nodeInfoMap[nodeID] = nodeInfoWrapper{
 		NodeInfo: nodeInfo,
 		evictAt:  time.Now().Add(r.ttl),
@@ -165,25 +137,6 @@ func (r *NodeInfoStore) List(ctx context.Context) ([]models.NodeInfo, error) {
 	return nodeInfos, nil
 }
 
-func (r *NodeInfoStore) ListForEngine(ctx context.Context, engine string) ([]models.NodeInfo, error) {
-	r.mu.RLock()
-	defer r.mu.RUnlock()
-	var nodeInfos []models.NodeInfo
-	var toEvict []nodeInfoWrapper
-	for nodeID := range r.engineNodeIDMap[engine] {
-		nodeInfo := r.nodeInfoMap[nodeID]
-		if time.Now().After(nodeInfo.evictAt) {
-			toEvict = append(toEvict, nodeInfo)
-		} else {
-			nodeInfos = append(nodeInfos, nodeInfo.NodeInfo)
-		}
-	}
-	if len(toEvict) > 0 {
-		go r.evict(ctx, toEvict...)
-	}
-	return nodeInfos, nil
-}
-
 func (r *NodeInfoStore) Delete(ctx context.Context, nodeID string) error {
 	r.mu.Lock()
 	defer r.mu.Unlock()
@@ -207,13 +160,6 @@ func (r *NodeInfoStore) evict(ctx context.Context, infoWrappers ...nodeInfoWrapp
 }
 
 func (r *NodeInfoStore) doDelete(ctx context.Context, nodeID string) error {
-	nodeInfo, ok := r.nodeInfoMap[nodeID]
-	if !ok {
-		return nil
-	}
-	for _, engine := range nodeInfo.ComputeNodeInfo.ExecutionEngines {
-		delete(r.engineNodeIDMap[engine], nodeID)
-	}
 	delete(r.nodeInfoMap, nodeID)
 	return nil
 }
diff --git a/pkg/routing/types.go b/pkg/routing/types.go
index 741c3352e6..c947dbd21a 100644
--- a/pkg/routing/types.go
+++ b/pkg/routing/types.go
@@ -18,8 +18,6 @@ type NodeInfoStore interface {
 	GetByPrefix(ctx context.Context, prefix string) (models.NodeInfo, error)
 	// List returns a list of nodes
 	List(ctx context.Context) ([]models.NodeInfo, error)
-	// ListForEngine returns a list of nodes that support the given engine.
-	ListForEngine(ctx context.Context, engine string) ([]models.NodeInfo, error)
 	// Delete deletes a node info from the repo.
 	Delete(ctx context.Context, nodeID string) error
 }


Test patch:
diff --git a/cmd/cli/serve/timeout_test.go b/cmd/cli/serve/timeout_test.go
index 20ebc63715..508190b986 100644
--- a/cmd/cli/serve/timeout_test.go
+++ b/cmd/cli/serve/timeout_test.go
@@ -46,7 +46,7 @@ func (s *ServeSuite) TestNoTimeoutSetOrApplied() {
 		)
 
 		s.Run(name, func() {
-			args := []string{}
+			args := []string{"--node-type", "requester,compute"}
 			if tc.configuredMax != nil {
 				args = append(args, "--max-job-execution-timeout", tc.configuredMax.String())
 			}
diff --git a/pkg/orchestrator/selection/discovery/chained_test.go b/pkg/orchestrator/selection/discovery/chained_test.go
index f0ed2db84c..88673d74d2 100644
--- a/pkg/orchestrator/selection/discovery/chained_test.go
+++ b/pkg/orchestrator/selection/discovery/chained_test.go
@@ -34,21 +34,21 @@ func TestChainedSuite(t *testing.T) {
 	suite.Run(t, new(ChainedSuite))
 }
 
-func (s *ChainedSuite) TestFindNodes() {
+func (s *ChainedSuite) TestListNodes() {
 	s.chain.Add(NewFixedDiscoverer(s.peerID1))
 	s.chain.Add(NewFixedDiscoverer(s.peerID2))
 	s.chain.Add(NewFixedDiscoverer(s.peerID3))
 
-	peerIDs, err := s.chain.FindNodes(context.Background(), models.Job{})
+	peerIDs, err := s.chain.ListNodes(context.Background())
 	s.NoError(err)
 	s.ElementsMatch([]models.NodeInfo{s.peerID1, s.peerID2, s.peerID3}, peerIDs)
 }
 
-func (s *ChainedSuite) TestFindNodes_Overlap() {
+func (s *ChainedSuite) TestListNodes_Overlap() {
 	s.chain.Add(NewFixedDiscoverer(s.peerID1, s.peerID2))
 	s.chain.Add(NewFixedDiscoverer(s.peerID2, s.peerID3))
 
-	peerIDs, err := s.chain.FindNodes(context.Background(), models.Job{})
+	peerIDs, err := s.chain.ListNodes(context.Background())
 	s.NoError(err)
 	s.ElementsMatch([]models.NodeInfo{s.peerID1, s.peerID2, s.peerID3}, peerIDs)
 }
@@ -57,7 +57,7 @@ func (s *ChainedSuite) TestHandle_Error() {
 	s.chain.Add(NewFixedDiscoverer(s.peerID1, s.peerID2))
 	s.chain.Add(newBadDiscoverer())
 	s.chain.Add(NewFixedDiscoverer(s.peerID3))
-	_, err := s.chain.FindNodes(context.Background(), models.Job{})
+	_, err := s.chain.ListNodes(context.Background())
 	s.Error(err)
 }
 
@@ -67,7 +67,7 @@ func (s *ChainedSuite) TestHandle_IgnoreError() {
 	s.chain.Add(newBadDiscoverer())
 	s.chain.Add(NewFixedDiscoverer(s.peerID3))
 
-	peerIDs, err := s.chain.FindNodes(context.Background(), models.Job{})
+	peerIDs, err := s.chain.ListNodes(context.Background())
 	s.NoError(err)
 	s.ElementsMatch([]models.NodeInfo{s.peerID1, s.peerID2, s.peerID3}, peerIDs)
 }
@@ -79,10 +79,6 @@ func newBadDiscoverer() *badDiscoverer {
 	return &badDiscoverer{}
 }
 
-func (b *badDiscoverer) FindNodes(context.Context, models.Job) ([]models.NodeInfo, error) {
-	return nil, errors.New("bad discoverer")
-}
-
 func (b *badDiscoverer) ListNodes(context.Context) ([]models.NodeInfo, error) {
 	return nil, errors.New("bad discoverer")
 }
diff --git a/pkg/orchestrator/selection/discovery/store_test.go b/pkg/orchestrator/selection/discovery/store_test.go
index 0a8ea24e8a..6f32a7ea75 100644
--- a/pkg/orchestrator/selection/discovery/store_test.go
+++ b/pkg/orchestrator/selection/discovery/store_test.go
@@ -34,30 +34,29 @@ func TestStoreNodeDiscovererSuite(t *testing.T) {
 	suite.Run(t, new(StoreNodeDiscovererSuite))
 }
 
-func (s *StoreNodeDiscovererSuite) TestFindNodes() {
+func (s *StoreNodeDiscovererSuite) TestListNodes() {
 	ctx := context.Background()
 	nodeInfo1 := generateNodeInfo("node1", models.EngineDocker)
-	nodeInfo2 := generateNodeInfo("node2", models.EngineDocker, models.EngineWasm)
 	s.NoError(s.store.Add(ctx, nodeInfo1))
-	s.NoError(s.store.Add(ctx, nodeInfo2))
 
 	// both nodes are returned when asked for docker nodes
 	job := mock.Job()
 	job.Task().Engine.Type = models.EngineDocker
 
-	peerIDs, err := s.discoverer.FindNodes(context.Background(), *job)
+	peerIDs, err := s.discoverer.ListNodes(context.Background())
 	s.NoError(err)
-	s.ElementsMatch([]models.NodeInfo{nodeInfo1, nodeInfo2}, peerIDs)
+	s.ElementsMatch([]models.NodeInfo{nodeInfo1}, peerIDs)
 
+	nodeInfo2 := generateNodeInfo("node2", models.EngineDocker, models.EngineWasm)
+	s.NoError(s.store.Add(ctx, nodeInfo2))
 	// only node2 is returned when asked for noop nodes
-	job2 := mock.Job()
-	peerIDs, err = s.discoverer.FindNodes(context.Background(), *job2)
+	peerIDs, err = s.discoverer.ListNodes(context.Background())
 	s.NoError(err)
-	s.Empty(peerIDs)
+	s.ElementsMatch([]models.NodeInfo{nodeInfo1, nodeInfo2}, peerIDs)
 }
 
-func (s *StoreNodeDiscovererSuite) TestFindNodes_Empty() {
-	peerIDs, err := s.discoverer.FindNodes(context.Background(), *mock.Job())
+func (s *StoreNodeDiscovererSuite) TestListNodes_Empty() {
+	peerIDs, err := s.discoverer.ListNodes(context.Background())
 	s.NoError(err)
 	s.Empty(peerIDs)
 }
diff --git a/pkg/routing/inmemory/inmemory_test.go b/pkg/routing/inmemory/inmemory_test.go
index 6ed2adb513..5c0544a7ac 100644
--- a/pkg/routing/inmemory/inmemory_test.go
+++ b/pkg/routing/inmemory/inmemory_test.go
@@ -119,24 +119,6 @@ func (s *InMemoryNodeInfoStoreSuite) Test_List() {
 	s.ElementsMatch([]models.NodeInfo{nodeInfo0, nodeInfo1}, allNodeInfos)
 }
 
-func (s *InMemoryNodeInfoStoreSuite) Test_ListForEngine() {
-	ctx := context.Background()
-	nodeInfo0 := generateNodeInfo(s.T(), nodeIDs[0], models.EngineDocker)
-	nodeInfo1 := generateNodeInfo(s.T(), nodeIDs[1], models.EngineWasm)
-	nodeInfo2 := generateNodeInfo(s.T(), nodeIDs[2], models.EngineDocker, models.EngineWasm)
-	s.NoError(s.store.Add(ctx, nodeInfo0))
-	s.NoError(s.store.Add(ctx, nodeInfo1))
-	s.NoError(s.store.Add(ctx, nodeInfo2))
-
-	dockerNodes, err := s.store.ListForEngine(ctx, models.EngineDocker)
-	s.NoError(err)
-	s.ElementsMatch([]models.NodeInfo{nodeInfo0, nodeInfo2}, dockerNodes)
-
-	wasmNodes, err := s.store.ListForEngine(ctx, models.EngineWasm)
-	s.NoError(err)
-	s.ElementsMatch([]models.NodeInfo{nodeInfo1, nodeInfo2}, wasmNodes)
-}
-
 func (s *InMemoryNodeInfoStoreSuite) Test_Delete() {
 	ctx := context.Background()
 	nodeInfo0 := generateNodeInfo(s.T(), nodeIDs[0], models.EngineDocker)
@@ -146,23 +128,15 @@ func (s *InMemoryNodeInfoStoreSuite) Test_Delete() {
 
 	// delete first node
 	s.NoError(s.store.Delete(ctx, nodeInfo0.ID()))
-	dockerNodes, err := s.store.ListForEngine(ctx, models.EngineDocker)
-	s.NoError(err)
-	s.ElementsMatch([]models.NodeInfo{nodeInfo1}, dockerNodes)
-
-	wasmNodes, err := s.store.ListForEngine(ctx, models.EngineWasm)
+	nodes, err := s.store.List(ctx)
 	s.NoError(err)
-	s.ElementsMatch([]models.NodeInfo{nodeInfo1}, wasmNodes)
+	s.ElementsMatch([]models.NodeInfo{nodeInfo1}, nodes)
 
 	// delete second node
 	s.NoError(s.store.Delete(ctx, nodeInfo1.ID()))
-	dockerNodes, err = s.store.ListForEngine(ctx, models.EngineDocker)
-	s.NoError(err)
-	s.Empty(dockerNodes)
-
-	wasmNodes, err = s.store.ListForEngine(ctx, models.EngineWasm)
+	nodes, err = s.store.List(ctx)
 	s.NoError(err)
-	s.Empty(wasmNodes)
+	s.Empty(nodes)
 }
 
 func (s *InMemoryNodeInfoStoreSuite) Test_Replace() {
@@ -182,15 +156,6 @@ func (s *InMemoryNodeInfoStoreSuite) Test_Replace() {
 	allNodeInfos, err := s.store.List(ctx)
 	s.NoError(err)
 	s.ElementsMatch([]models.NodeInfo{nodeInfo1}, allNodeInfos)
-
-	// test ListForEngine
-	dockerNodes, err := s.store.ListForEngine(ctx, models.EngineDocker)
-	s.NoError(err)
-	s.Empty(dockerNodes)
-
-	wasmNodes, err := s.store.ListForEngine(ctx, models.EngineWasm)
-	s.NoError(err)
-	s.ElementsMatch([]models.NodeInfo{nodeInfo1}, wasmNodes)
 }
 
 func (s *InMemoryNodeInfoStoreSuite) Test_Eviction() {
diff --git a/pkg/test/devstack/disabled_feature_test.go b/pkg/test/devstack/disabled_feature_test.go
index 6a15ed44a1..b4ec92f552 100644
--- a/pkg/test/devstack/disabled_feature_test.go
+++ b/pkg/test/devstack/disabled_feature_test.go
@@ -26,8 +26,8 @@ func disabledTestSpec(t testing.TB) scenario.Scenario {
 	return scenario.Scenario{
 		Stack: &scenario.StackConfig{
 			DevStackOptions: &devstack.DevStackOptions{
-				NumberOfRequesterOnlyNodes: 1,
-				NumberOfComputeOnlyNodes:   1,
+				NumberOfHybridNodes:      1,
+				NumberOfComputeOnlyNodes: 1,
 			},
 		},
 		Spec: scenario.WasmHelloWorld(t).Spec,
diff --git a/pkg/test/devstack/target_all_test.go b/pkg/test/devstack/target_all_test.go
index d1c5f0e55c..af7e71e720 100644
--- a/pkg/test/devstack/target_all_test.go
+++ b/pkg/test/devstack/target_all_test.go
@@ -50,9 +50,7 @@ func (suite *TargetAllSuite) TestCanTargetZeroNodes() {
 func (suite *TargetAllSuite) TestCanTargetSingleNode() {
 	testCase := scenario.Scenario{
 		Stack: &scenario.StackConfig{DevStackOptions: &devstack.DevStackOptions{
-			NumberOfHybridNodes:        0,
-			NumberOfRequesterOnlyNodes: 1,
-			NumberOfComputeOnlyNodes:   1,
+			NumberOfHybridNodes: 1,
 		}},
 		Spec:          testutils.MakeSpecWithOpts(suite.T()),
 		Deal:          model.Deal{TargetingMode: model.TargetAll},
@@ -71,9 +69,8 @@ func (suite *TargetAllSuite) TestCanTargetSingleNode() {
 func (suite *TargetAllSuite) TestCanTargetMultipleNodes() {
 	testCase := scenario.Scenario{
 		Stack: &scenario.StackConfig{DevStackOptions: &devstack.DevStackOptions{
-			NumberOfHybridNodes:        0,
-			NumberOfRequesterOnlyNodes: 1,
-			NumberOfComputeOnlyNodes:   5,
+			NumberOfHybridNodes:      1,
+			NumberOfComputeOnlyNodes: 4,
 		}},
 		Spec:          testutils.MakeSpecWithOpts(suite.T()),
 		Deal:          model.Deal{TargetingMode: model.TargetAll},
@@ -95,9 +92,8 @@ func (suite *TargetAllSuite) TestPartialFailure() {
 	testCase := scenario.Scenario{
 		Stack: &scenario.StackConfig{
 			DevStackOptions: &devstack.DevStackOptions{
-				NumberOfHybridNodes:        0,
-				NumberOfRequesterOnlyNodes: 1,
-				NumberOfComputeOnlyNodes:   2,
+				NumberOfHybridNodes:      1,
+				NumberOfComputeOnlyNodes: 1,
 			},
 			ExecutorConfig: noop.ExecutorConfig{
 				ExternalHooks: noop.ExecutorConfigExternalHooks{