diff --git a/common/monitor/progress.go b/common/monitor/progress.go index 9e2c032a7..f812782c1 100644 --- a/common/monitor/progress.go +++ b/common/monitor/progress.go @@ -158,6 +158,14 @@ func (s *Span) Progress() Progress { } parentCount += childTotal * child.Count / child.Total } + // Clamp parentCount to parentTotal. The "zero-total child is complete" + // fallback above can otherwise push parentCount beyond parentTotal when + // the parent has already consumed its own count and a running child is + // reporting no work units, which would surface as >100% progress in the + // dashboard. + if parentCount > parentTotal { + parentCount = parentTotal + } return Progress{ Name: s.name, Status: s.status, diff --git a/common/monitor/progress_overflow_test.go b/common/monitor/progress_overflow_test.go new file mode 100644 index 000000000..8277ef8d7 --- /dev/null +++ b/common/monitor/progress_overflow_test.go @@ -0,0 +1,25 @@ +package monitor + +import ( + "context" + "testing" +) + +// TestProgress_ZeroTotalChildDoesNotExceed100Percent pins the behavior of the +// zero-total child fallback in Span.Progress. Before the clamp, a parent +// with total=2,count=2 plus a running child with total=0 would return +// Count=3,Total=2 (150%), which confused the dashboard progress bar. +// +// The clamp ensures Count <= Total for every case the fallback handles. +func TestProgress_ZeroTotalChildDoesNotExceed100Percent(t *testing.T) { + _, parent := Start(context.Background(), "parent", 2) + parent.Add(2) // parent's own work is complete + + ctx := context.WithValue(context.Background(), spanKeyName, parent) + _, _ = Start(ctx, "child", 0) + + got := parent.Progress() + if got.Count > got.Total { + t.Fatalf("Progress.Count (%d) exceeds Progress.Total (%d)", got.Count, got.Total) + } +}