fix(landscape): bound node fan-out, cover override resolution, test the run consumer

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 13:49:27 +05:00
parent 5fba77b59c
commit d74277552f
4 changed files with 188 additions and 8 deletions

View File

@@ -67,8 +67,11 @@ func (h landscapeHandler) Handle(ctx context.Context, d amqp.Delivery) error {
if err != nil {
// The delivery is nacked without requeue (see Consumer.dispatch), so the
// run will not be retried. Record the failure on the run itself;
// otherwise it stays "running" forever. Best-effort: log if marking
// fails, but still return the original error so the delivery is nacked.
// otherwise it stays "running" forever. This covers the errors Execute
// returns before it can mark the run itself (a missing run, a failed
// status write), since Execute already marks the run failed for every
// node failure. Best-effort: log if marking fails, but still return the
// original error so the delivery is nacked.
if markErr := h.proc.MarkRunFailed(ctx, job.RunID, err.Error()); markErr != nil {
h.log.Error("mark landscape run failed", "run_id", job.RunID, "error", markErr)
}

View File

@@ -0,0 +1,90 @@
package rabbitmq
import (
"context"
"encoding/json"
"errors"
"testing"
"github.com/google/uuid"
amqp "github.com/rabbitmq/amqp091-go"
)
// fakeLandscapeProcessor records MarkRunFailed calls and returns a preset error
// from Execute.
type fakeLandscapeProcessor struct {
runErr error
executed []uuid.UUID
failedID uuid.UUID
failedWith string
failedCnt int
}
func (f *fakeLandscapeProcessor) Execute(_ context.Context, runID uuid.UUID) error {
f.executed = append(f.executed, runID)
return f.runErr
}
func (f *fakeLandscapeProcessor) MarkRunFailed(_ context.Context, runID uuid.UUID, reason string) error {
f.failedCnt++
f.failedID = runID
f.failedWith = reason
return nil
}
func landscapeDelivery(t *testing.T, id uuid.UUID) amqp.Delivery {
t.Helper()
body, err := json.Marshal(LandscapeJob{RunID: id})
if err != nil {
t.Fatalf("marshal job: %v", err)
}
return amqp.Delivery{Body: body}
}
func TestLandscapeHandler_MarksRunFailedOnExecuteError(t *testing.T) {
id := uuid.New()
fp := &fakeLandscapeProcessor{runErr: errors.New("node 00: grid mismatch")}
h := NewLandscapeRunHandler(fp, discardLogger())
err := h.Handle(context.Background(), landscapeDelivery(t, id))
if err == nil {
t.Fatal("expected the run error to be returned so the delivery is nacked")
}
if len(fp.executed) != 1 || fp.executed[0] != id {
t.Fatalf("Execute called with %v, want [%s]", fp.executed, id)
}
if fp.failedCnt != 1 {
t.Fatalf("expected MarkRunFailed to be called once, got %d", fp.failedCnt)
}
if fp.failedID != id {
t.Fatalf("MarkRunFailed called with wrong id: got %s want %s", fp.failedID, id)
}
if fp.failedWith != "node 00: grid mismatch" {
t.Fatalf("MarkRunFailed called with wrong reason: %q", fp.failedWith)
}
}
func TestLandscapeHandler_DoesNotMarkFailedOnSuccess(t *testing.T) {
fp := &fakeLandscapeProcessor{runErr: nil}
h := NewLandscapeRunHandler(fp, discardLogger())
if err := h.Handle(context.Background(), landscapeDelivery(t, uuid.New())); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if fp.failedCnt != 0 {
t.Fatalf("expected MarkRunFailed not to be called on success, got %d", fp.failedCnt)
}
}
func TestLandscapeHandler_UndecodableBodyIsNacked(t *testing.T) {
fp := &fakeLandscapeProcessor{}
h := NewLandscapeRunHandler(fp, discardLogger())
err := h.Handle(context.Background(), amqp.Delivery{Body: []byte("not json")})
if err == nil {
t.Fatal("expected a decode error so the delivery is nacked")
}
if len(fp.executed) != 0 {
t.Fatalf("Execute must not run for an undecodable body, got %v", fp.executed)
}
}