diff --git a/fs/accounting/stats.go b/fs/accounting/stats.go index b029cb590..f808e1fdf 100644 --- a/fs/accounting/stats.go +++ b/fs/accounting/stats.go @@ -229,6 +229,11 @@ func (s *StatsInfo) totalDuration() time.Duration { return s.oldDuration + timeRanges.total() } +const ( + etaMaxSeconds = (1<<63 - 1) / int64(time.Second) // Largest possible ETA as number of seconds + etaMax = time.Duration(etaMaxSeconds) * time.Second // Largest possible ETA, which is in second precision, representing "292y24w3d23h47m16s" +) + // eta returns the ETA of the current operation, // rounded to full seconds. // If the ETA cannot be determined 'ok' returns false. @@ -240,11 +245,17 @@ func eta(size, total int64, rate float64) (eta time.Duration, ok bool) { if remaining < 0 { return 0, false } - seconds := float64(remaining) / rate + seconds := int64(float64(remaining) / rate) if seconds < 0 { - seconds = 0 + // Got Int64 overflow + eta = etaMax + } else if seconds >= etaMaxSeconds { + // Would get Int64 overflow if converting from seconds to Duration (nanoseconds) + eta = etaMax + } else { + eta = time.Duration(seconds) * time.Second } - return time.Second * time.Duration(seconds), true + return eta, true } // etaString returns the ETA of the current operation, @@ -255,6 +266,9 @@ func etaString(done, total int64, rate float64) string { if !ok { return "-" } + if d == etaMax { + return "-" + } return fs.Duration(d).ReadableString() } diff --git a/fs/accounting/stats_test.go b/fs/accounting/stats_test.go index c22f0031b..b1a3cbb2a 100644 --- a/fs/accounting/stats_test.go +++ b/fs/accounting/stats_test.go @@ -31,6 +31,9 @@ func TestETA(t *testing.T) { {size: 0, total: 1.5 * 86400, rate: 1.0, wantETA: 1.5 * 86400 * time.Second, wantOK: true, wantString: "1d12h"}, {size: 0, total: 95000, rate: 1.0, wantETA: 95000 * time.Second, wantOK: true, wantString: "1d2h23m20s"}, // Standard Duration String Cases + {size: 0, total: 1, rate: 2.0, wantETA: 0, wantOK: true, wantString: "0s"}, + {size: 0, total: 1, rate: 1.0, wantETA: time.Second, wantOK: true, wantString: "1s"}, + {size: 0, total: 1, rate: 0.5, wantETA: 2 * time.Second, wantOK: true, wantString: "2s"}, {size: 0, total: 100, rate: 1.0, wantETA: 100 * time.Second, wantOK: true, wantString: "1m40s"}, {size: 50, total: 100, rate: 1.0, wantETA: 50 * time.Second, wantOK: true, wantString: "50s"}, {size: 100, total: 100, rate: 1.0, wantETA: 0 * time.Second, wantOK: true, wantString: "0s"}, @@ -41,10 +44,15 @@ func TestETA(t *testing.T) { {size: 10, total: 20, rate: 0.0, wantETA: 0, wantOK: false, wantString: "-"}, {size: 10, total: 20, rate: -1.0, wantETA: 0, wantOK: false, wantString: "-"}, {size: 0, total: 0, rate: 1.0, wantETA: 0, wantOK: false, wantString: "-"}, + // Extreme Cases + {size: 0, total: (1 << 63) - 1, rate: 1.0, wantETA: (time.Duration((1<<63)-1) / time.Second) * time.Second, wantOK: true, wantString: "-"}, + {size: 0, total: ((1 << 63) - 1) / int64(time.Second), rate: 1.0, wantETA: (time.Duration((1<<63)-1) / time.Second) * time.Second, wantOK: true, wantString: "-"}, + {size: 0, total: ((1<<63)-1)/int64(time.Second) - 1, rate: 1.0, wantETA: (time.Duration((1<<63)-1)/time.Second - 1) * time.Second, wantOK: true, wantString: "292y24w3d23h47m15s"}, + {size: 0, total: ((1<<63)-1)/int64(time.Second) - 1, rate: 0.1, wantETA: (time.Duration((1<<63)-1) / time.Second) * time.Second, wantOK: true, wantString: "-"}, } { t.Run(fmt.Sprintf("size=%d/total=%d/rate=%f", test.size, test.total, test.rate), func(t *testing.T) { gotETA, gotOK := eta(test.size, test.total, test.rate) - assert.Equal(t, test.wantETA, gotETA) + assert.Equal(t, int64(test.wantETA), int64(gotETA)) assert.Equal(t, test.wantOK, gotOK) gotString := etaString(test.size, test.total, test.rate) assert.Equal(t, test.wantString, gotString)