// NormalizePriceHistory de-duplicates and sorts instance prices, most
// recent first.
-//
-// The provided slice is modified in place.
func NormalizePriceHistory(prices []InstancePrice) []InstancePrice {
+ // copy provided slice instead of modifying it in place
+ prices = append([]InstancePrice(nil), prices...)
// sort by timestamp, newest first
sort.Slice(prices, func(i, j int) bool {
return prices[i].StartTime.After(prices[j].StartTime)
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package cloud
+
+import (
+ "testing"
+ "time"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type cloudSuite struct{}
+
+var _ = Suite(&cloudSuite{})
+
+func (s *cloudSuite) TestNormalizePriceHistory(c *C) {
+ t0, err := time.Parse(time.RFC3339, "2023-01-01T01:00:00Z")
+ c.Assert(err, IsNil)
+ h := []InstancePrice{
+ {t0.Add(1 * time.Minute), 1.0},
+ {t0.Add(4 * time.Minute), 1.2}, // drop: unchanged price
+ {t0.Add(5 * time.Minute), 1.1},
+ {t0.Add(3 * time.Minute), 1.2},
+ {t0.Add(5 * time.Minute), 1.1}, // drop: duplicate
+ {t0.Add(2 * time.Minute), 1.0}, // drop: out of order, unchanged price
+ }
+ c.Check(NormalizePriceHistory(h), DeepEquals, []InstancePrice{h[2], h[3], h[0]})
+}
}
cr.pricesLock.Lock()
defer cr.pricesLock.Unlock()
+ var lastKnown time.Time
+ if len(cr.prices) > 0 {
+ lastKnown = cr.prices[0].StartTime
+ }
cr.prices = cloud.NormalizePriceHistory(append(prices, cr.prices...))
+ for i := len(cr.prices) - 1; i >= 0; i-- {
+ price := cr.prices[i]
+ if price.StartTime.After(lastKnown) {
+ cr.CrunchLog.Printf("Instance price changed to %#.3g at %s", price.Price, price.StartTime.UTC())
+ }
+ }
}
func (cr *ContainerRunner) calculateCost(now time.Time) float64 {
"fmt"
"io"
"io/ioutil"
+ "log"
"os"
"os/exec"
"regexp"
defer func(s string) { lockdir = s }(lockdir)
lockdir = c.MkDir()
now := time.Now()
- cr := ContainerRunner{costStartTime: now.Add(-time.Hour)}
+ cr := s.runner
+ cr.costStartTime = now.Add(-time.Hour)
+ var logbuf bytes.Buffer
+ cr.CrunchLog.Immediate = log.New(&logbuf, "", 0)
// if there's no InstanceType env var, cost is calculated as 0
os.Unsetenv("InstanceType")
// next update (via --list + SIGUSR2) tells us the spot price
// increased to $3/h 15 minutes ago
j, err = json.Marshal([]cloud.InstancePrice{
+ {StartTime: now.Add(-time.Hour / 3), Price: 2.0}, // dup of -time.Hour/2 price
{StartTime: now.Add(-time.Hour / 4), Price: 3.0},
})
c.Assert(err, IsNil)
cr.loadPrices()
cost = cr.calculateCost(now)
c.Check(cost, Equals, 1.0/2+2.0/4+3.0/4)
+
+ c.Logf("%s", logbuf.String())
+ c.Check(logbuf.String(), Matches, `(?ms).*Instance price changed to 1\.00 at 20.* changed to 2\.00 .* changed to 3\.00 .*`)
+ c.Check(logbuf.String(), Not(Matches), `(?ms).*changed to 2\.00 .* changed to 2\.00 .*`)
}
type FakeProcess struct {