[ARVADOS] created: 2.1.0-1384-g34287594a

Git user git at public.arvados.org
Sun Sep 26 15:05:54 UTC 2021


        at  34287594a9c07bd3f001a6cf22e05a22646249b8 (commit)


commit 34287594a9c07bd3f001a6cf22e05a22646249b8
Author: Ward Vandewege <ward at jhvc.com>
Date:   Sun Sep 26 11:04:55 2021 -0400

    17695: only get the spot price history once per toplevel container
           request.
    
    Arvados-DCO-1.1-Signed-off-by: Ward Vandewege <ward at curii.com>

diff --git a/lib/costanalyzer/aws-spot.go b/lib/costanalyzer/aws-spot.go
index 647753e46..bdacf67c7 100644
--- a/lib/costanalyzer/aws-spot.go
+++ b/lib/costanalyzer/aws-spot.go
@@ -32,22 +32,18 @@ func (s SpotPriceHistory) Len() int           { return len(s) }
 func (s SpotPriceHistory) Less(i, j int) bool { return s[i].Timestamp.Before(*s[j].Timestamp) }
 func (s SpotPriceHistory) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
 
-func CalculateAWSSpotPrice(container arvados.Container, size string) (float64, error) {
+func GetAWSSpotPriceHistory(container arvados.Container) (map[string]SpotPriceHistory, error) {
+	var history SpotPriceHistory
 	// FIXME hardcoded region, does this matter?
 	svc := ec2.New(session.New(&aws.Config{
 		Region: aws.String("us-east-1"),
 		//		LogLevel: aws.LogLevel(aws.LogDebugWithHTTPBody),
 	}))
 	// FIXME should we ask for a specific AvailabilityZone here? We don't even have one in the config (it is derived from the network id I think).
-	//end := container.FinishedAt.Add(time.Hour * time.Duration(24))
 	input := &ec2.DescribeSpotPriceHistoryInput{
 		AvailabilityZone: aws.String("us-east-1a"),
-		//EndTime: aws.Time(end),
-		EndTime: container.FinishedAt,
+		EndTime:          container.FinishedAt,
 		//DryRun:  aws.Bool(true),
-		InstanceTypes: []*string{
-			aws.String(size),
-		},
 		ProductDescriptions: []*string{
 			aws.String("Linux/UNIX (Amazon VPC)"),
 		},
@@ -55,29 +51,81 @@ func CalculateAWSSpotPrice(container arvados.Container, size string) (float64, e
 	}
 	//fmt.Printf("%#v\n", input)
 
-	result, err := svc.DescribeSpotPriceHistory(input)
-	if err != nil {
-		if aerr, ok := err.(awserr.Error); ok {
-			switch aerr.Code() {
-			default:
-				fmt.Println(aerr.Error())
+	for {
+		result, err := svc.DescribeSpotPriceHistory(input)
+		if err != nil {
+			if aerr, ok := err.(awserr.Error); ok {
+				switch aerr.Code() {
+				default:
+					fmt.Println(aerr.Error())
+				}
+			} else {
+				fmt.Println(err.Error())
 			}
-		} else {
-			// Print the error, cast err to awserr.Error to get the Code and
-			// Message from an error.
-			fmt.Println(err.Error())
+			return nil, err
+		}
+		history = append(history, result.SpotPriceHistory...)
+
+		if *result.NextToken == "" {
+			// No more pages
+			break
+		}
+		input.NextToken = result.NextToken
+	}
+
+	historyMap := make(map[string]SpotPriceHistory)
+
+	for _, sph := range history {
+		if _, ok := historyMap[*sph.InstanceType]; !ok {
+			historyMap[*sph.InstanceType] = make(SpotPriceHistory, 0)
+		}
+		historyMap[*sph.InstanceType] = append(historyMap[*sph.InstanceType], sph)
+	}
+
+	// Sort all the SpotPriceHistories
+	for instanceType := range historyMap {
+		sort.Sort(historyMap[instanceType])
+	}
+
+	return historyMap, nil
+}
+
+func findSpotPriceHistoryStart(container arvados.Container, history SpotPriceHistory) SpotPriceHistory {
+	pos := len(history) / 2
+	oldPos := pos
+
+	for {
+		if history[pos].Timestamp.After(*container.StartedAt) {
+			// reduce pos
+			oldPos = pos
+			pos = pos - pos/2
 		}
-		return 0, err
+		if len(history) > pos+1 && history[pos+1].Timestamp.Before(*container.StartedAt) {
+			// increase pos
+			oldPos = pos
+			pos = pos + pos/2
+		}
+
+		if oldPos == pos {
+			break
+		}
+		fmt.Printf("pos: %d, oldPos: %d\n", pos, oldPos)
 	}
 
-	sort.Sort(SpotPriceHistory(result.SpotPriceHistory))
+	return history[pos:]
+}
 
+func CalculateAWSSpotPrice(container arvados.Container, fullHistory SpotPriceHistory) (float64, error) {
 	//fmt.Printf("%#v\n", result)
+	history := findSpotPriceHistoryStart(container, fullHistory)
 	var total float64
-	last := result.SpotPriceHistory[0]
+	last := history[0]
 	last.Timestamp = container.StartedAt
 	//fmt.Printf("LAST: %#v\n", last)
-	for _, s := range result.SpotPriceHistory[1:] {
+	for _, s := range history[1:] {
+		if s.Timestamp.After(*container.FinishedAt) {
+			break
+		}
 		//fmt.Printf("%#v\n", s)
 		delta := s.Timestamp.Sub(*last.Timestamp)
 		price, err := strconv.ParseFloat(*last.SpotPrice, 64)
diff --git a/lib/costanalyzer/costanalyzer.go b/lib/costanalyzer/costanalyzer.go
index e35c3d0cf..26117e884 100644
--- a/lib/costanalyzer/costanalyzer.go
+++ b/lib/costanalyzer/costanalyzer.go
@@ -201,7 +201,7 @@ func ensureDirectory(logger *logrus.Logger, dir string) (err error) {
 	return
 }
 
-func addContainerLine(logger *logrus.Logger, node nodeInfo, cr arvados.ContainerRequest, container arvados.Container) (string, consumption, error) {
+func addContainerLine(logger *logrus.Logger, node nodeInfo, cr arvados.ContainerRequest, container arvados.Container, history map[string]SpotPriceHistory) (string, consumption, error) {
 	var csv string
 	var containerConsumption consumption
 	csv = cr.UUID + ","
@@ -236,7 +236,7 @@ func addContainerLine(logger *logrus.Logger, node nodeInfo, cr arvados.Container
 		containerConsumption.cost = delta.Seconds() / 3600 * price
 		csv += size + "," + fmt.Sprintf("%+v", node.Preemptible) + "," + strconv.FormatFloat(price, 'f', 8, 64) + "," + strconv.FormatFloat(containerConsumption.cost, 'f', 8, 64) + ",\n"
 	} else {
-		sum, err := CalculateAWSSpotPrice(container, size)
+		sum, err := CalculateAWSSpotPrice(container, history[size])
 		containerConsumption.cost = sum
 		containerConsumption.savings = delta.Seconds()/3600*price - sum
 		csv += size + "," + fmt.Sprintf("%+v", node.Preemptible) + ",," + strconv.FormatFloat(sum, 'f', 8, 64) + "," + strconv.FormatFloat(containerConsumption.savings, 'f', 8, 64) + "\n"
@@ -427,8 +427,24 @@ func handleProject(logger *logrus.Logger, uuid string, arv *arvadosclient.Arvado
 	return
 }
 
+func getHistory(node nodeInfo, container arvados.Container, history map[string]SpotPriceHistory) (map[string]SpotPriceHistory, error) {
+	var err error
+	if node.Preemptible && history == nil {
+		// This is a preemptable instance, and we have not yet retrieved the spot
+		// price history for the duration of the toplevel workflow. We do this in
+		// one go and then use the spot price history to look up costs as needed
+		// without the need for additional AWS API requests.
+		history, err = GetAWSSpotPriceHistory(container)
+		if err != nil {
+			return nil, fmt.Errorf("error getting AWS spot price history: %s", err.Error())
+		}
+	}
+	return history, nil
+}
+
 func generateCrInfo(logger *logrus.Logger, uuid string, arv *arvadosclient.ArvadosClient, ac *arvados.Client, kc *keepclient.KeepClient, resultsDir string, cache bool) (cost map[string]consumption, err error) {
 
+	var history map[string]SpotPriceHistory
 	cost = make(map[string]consumption)
 
 	csv := "CR UUID,CR name,Container UUID,State,Started At,Finished At,Duration in seconds,Compute node type,Preemptible,Hourly node cost,Total cost,Savings (if spot)\n"
@@ -476,7 +492,13 @@ func generateCrInfo(logger *logrus.Logger, uuid string, arv *arvadosclient.Arvad
 		logger.Errorf("Skipping container request %s: error getting node %s: %s", cr.UUID, cr.UUID, err)
 		return nil, nil
 	}
-	tmpCsv, total, err = addContainerLine(logger, topNode, cr, container)
+
+	history, err = getHistory(topNode, container, history)
+	if err != nil {
+		return nil, err
+	}
+
+	tmpCsv, total, err = addContainerLine(logger, topNode, cr, container, history)
 	if err != nil {
 		return nil, fmt.Errorf("error adding container line: %s", err.Error())
 	}
@@ -507,22 +529,32 @@ func generateCrInfo(logger *logrus.Logger, uuid string, arv *arvadosclient.Arvad
 			logger.Infof("... %d of %d", i+1, len(childCrs.Items))
 		default:
 		}
-		// We've already calculated this child
-		/*if _, err := os.Stat(resultsDir + "/" + crUUID + ".csv"); err == nil {
-			continue
-		}*/
 		node, err := getNode(arv, ac, kc, cr2)
 		if err != nil {
 			logger.Errorf("Skipping container request %s: error getting node %s: %s", cr2.UUID, cr2.UUID, err)
 			continue
 		}
+		history, err = getHistory(node, container, history)
+		if err != nil {
+			return nil, err
+		}
+		/*if node.Preemptible && history == nil {
+			// This is a preemptable instance, and we have not yet retrieved the spot
+			// price history for the duration of the toplevel workflow. We do this in
+			// one go and then use the spot price history to look up costs as needed
+			// without the need for additional AWS API requests.
+			history, err = GetAWSSpotPriceHistory(container)
+			if err != nil {
+				return nil, fmt.Errorf("error getting AWS spot price history: %s", err.Error())
+			}
+		}*/
 		logger.Debug("Child container: " + cr2.ContainerUUID)
 		var c2 arvados.Container
 		err = loadObject(logger, ac, cr.UUID, cr2.ContainerUUID, cache, &c2)
 		if err != nil {
 			return nil, fmt.Errorf("error loading object %s: %s", cr2.ContainerUUID, err)
 		}
-		tmpCsv, tmpTotal, err = addContainerLine(logger, node, cr2, c2)
+		tmpCsv, tmpTotal, err = addContainerLine(logger, node, cr2, c2, history)
 		if err != nil {
 			return nil, fmt.Errorf("error adding container line: %s", err.Error())
 		}
@@ -674,7 +706,7 @@ func (c *command) costAnalyzer(prog string, args []string, logger *logrus.Logger
 		total.Add(v)
 	}
 
-	csv += "TOTAL," + strconv.FormatFloat(total.duration, 'f', 3, 64) + "," + strconv.FormatFloat(total.cost, 'f', 2, 64) + strconv.FormatFloat(total.savings, 'f', 2, 64) + "\n"
+	csv += "TOTAL," + strconv.FormatFloat(total.duration, 'f', 3, 64) + "," + strconv.FormatFloat(total.cost, 'f', 2, 64) + "," + strconv.FormatFloat(total.savings, 'f', 2, 64) + "\n"
 
 	if c.resultsDir != "" {
 		// Write the resulting CSV file

commit ca3c223f6c6e27c3fecbd25497404a991eafd10b
Author: Ward Vandewege <ward at jhvc.com>
Date:   Thu Sep 23 11:09:16 2021 -0400

    First implementation of costanalyzer aws spot support. It works but it
    is naive; it does not cache the spot price historical records at all.
    
    refs #17695
    
    Arvados-DCO-1.1-Signed-off-by: Ward Vandewege <ward at curii.com>

diff --git a/go.mod b/go.mod
index adca449b7..5910e172c 100644
--- a/go.mod
+++ b/go.mod
@@ -13,7 +13,7 @@ require (
 	github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 // indirect
 	github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
 	github.com/arvados/cgofuse v1.2.0-arvados1
-	github.com/aws/aws-sdk-go v1.25.30
+	github.com/aws/aws-sdk-go v1.40.47
 	github.com/aws/aws-sdk-go-v2 v0.23.0
 	github.com/bgentry/speakeasy v0.1.0 // indirect
 	github.com/bradleypeabody/godap v0.0.0-20170216002349-c249933bc092
@@ -59,7 +59,7 @@ require (
 	github.com/src-d/gcfg v1.3.0 // indirect
 	github.com/xanzy/ssh-agent v0.1.0 // indirect
 	golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
-	golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
+	golang.org/x/net v0.0.0-20210614182718-04defd469f4e
 	golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
 	golang.org/x/sys v0.0.0-20210603125802-9665404d3644
 	golang.org/x/tools v0.1.2 // indirect
@@ -71,7 +71,6 @@ require (
 	gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 // indirect
 	gopkg.in/src-d/go-git.v4 v4.0.0
 	gopkg.in/warnings.v0 v0.1.2 // indirect
-	gopkg.in/yaml.v2 v2.2.4 // indirect
 	rsc.io/getopt v0.0.0-20170811000552-20be20937449
 )
 
diff --git a/go.sum b/go.sum
index 2f575eae9..0165e6db2 100644
--- a/go.sum
+++ b/go.sum
@@ -52,6 +52,8 @@ github.com/arvados/yaml v0.0.0-20210427145106-92a1cab0904b/go.mod h1:RDklbk79AGW
 github.com/aws/aws-sdk-go v1.17.4/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 github.com/aws/aws-sdk-go v1.25.30 h1:I9qj6zW3mMfsg91e+GMSN/INcaX9tTFvr/l/BAHKaIY=
 github.com/aws/aws-sdk-go v1.25.30/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/aws/aws-sdk-go v1.40.47 h1:ZXayMFfPtODnSZRlMSmMYpiygac6PORNCPMjdGltcFg=
+github.com/aws/aws-sdk-go v1.40.47/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
 github.com/aws/aws-sdk-go-v2 v0.23.0 h1:+E1q1LLSfHSDn/DzOtdJOX+pLZE2HiNV2yO5AjZINwM=
 github.com/aws/aws-sdk-go-v2 v0.23.0/go.mod h1:2LhT7UgHOXK3UXONKI5OMgIyoQL6zTAw/jwIeX6yqzw=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@@ -159,6 +161,9 @@ github.com/jmcvetta/randutil v0.0.0-20150817122601-2bb1b664bcff h1:6NvhExg4omUC9
 github.com/jmcvetta/randutil v0.0.0-20150817122601-2bb1b664bcff/go.mod h1:ddfPX8Z28YMjiqoaJhNBzWHapTHXejnB5cDCUWDwriw=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
 github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
 github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
 github.com/johannesboyne/gofakes3 v0.0.0-20200716060623-6b2b4cb092cc h1:JJPhSHowepOF2+ElJVyb9jgt5ZyBkPMkPuhS0uODSFs=
@@ -206,6 +211,7 @@ github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtb
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
@@ -290,6 +296,7 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjN
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
@@ -319,6 +326,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210603125802-9665404d3644 h1:CA1DEQ4NdKphKeL70tvsWNdT5oFh1lOjihRcEDROi0I=
@@ -330,6 +338,7 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/lib/costanalyzer/aws-spot.go b/lib/costanalyzer/aws-spot.go
new file mode 100644
index 000000000..647753e46
--- /dev/null
+++ b/lib/costanalyzer/aws-spot.go
@@ -0,0 +1,104 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package costanalyzer
+
+import (
+	"fmt"
+	//	"strings"
+	"sort"
+	"strconv"
+	"time"
+
+	"git.arvados.org/arvados.git/sdk/go/arvados"
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/awserr"
+	"github.com/aws/aws-sdk-go/aws/session"
+	"github.com/aws/aws-sdk-go/service/ec2"
+)
+
+func parseTime(layout, value string) *time.Time {
+	t, err := time.Parse(layout, value)
+	if err != nil {
+		panic(err)
+	}
+	return &t
+}
+
+type SpotPriceHistory []*ec2.SpotPrice
+
+func (s SpotPriceHistory) Len() int           { return len(s) }
+func (s SpotPriceHistory) Less(i, j int) bool { return s[i].Timestamp.Before(*s[j].Timestamp) }
+func (s SpotPriceHistory) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
+
+func CalculateAWSSpotPrice(container arvados.Container, size string) (float64, error) {
+	// FIXME hardcoded region, does this matter?
+	svc := ec2.New(session.New(&aws.Config{
+		Region: aws.String("us-east-1"),
+		//		LogLevel: aws.LogLevel(aws.LogDebugWithHTTPBody),
+	}))
+	// FIXME should we ask for a specific AvailabilityZone here? We don't even have one in the config (it is derived from the network id I think).
+	//end := container.FinishedAt.Add(time.Hour * time.Duration(24))
+	input := &ec2.DescribeSpotPriceHistoryInput{
+		AvailabilityZone: aws.String("us-east-1a"),
+		//EndTime: aws.Time(end),
+		EndTime: container.FinishedAt,
+		//DryRun:  aws.Bool(true),
+		InstanceTypes: []*string{
+			aws.String(size),
+		},
+		ProductDescriptions: []*string{
+			aws.String("Linux/UNIX (Amazon VPC)"),
+		},
+		StartTime: container.StartedAt,
+	}
+	//fmt.Printf("%#v\n", input)
+
+	result, err := svc.DescribeSpotPriceHistory(input)
+	if err != nil {
+		if aerr, ok := err.(awserr.Error); ok {
+			switch aerr.Code() {
+			default:
+				fmt.Println(aerr.Error())
+			}
+		} else {
+			// Print the error, cast err to awserr.Error to get the Code and
+			// Message from an error.
+			fmt.Println(err.Error())
+		}
+		return 0, err
+	}
+
+	sort.Sort(SpotPriceHistory(result.SpotPriceHistory))
+
+	//fmt.Printf("%#v\n", result)
+	var total float64
+	last := result.SpotPriceHistory[0]
+	last.Timestamp = container.StartedAt
+	//fmt.Printf("LAST: %#v\n", last)
+	for _, s := range result.SpotPriceHistory[1:] {
+		//fmt.Printf("%#v\n", s)
+		delta := s.Timestamp.Sub(*last.Timestamp)
+		price, err := strconv.ParseFloat(*last.SpotPrice, 64)
+		if err != nil {
+			return 0, nil
+		}
+		total += delta.Seconds() / 3600 * price
+		last = s
+		/*fmt.Printf("YO\n")
+		fmt.Printf("%#v\n", s)
+		fmt.Printf("COST: %#v\n", price)
+		fmt.Printf("TOTAL: %#v\n", delta.Seconds()/3600*price)
+		fmt.Printf("SECONDS: %#v\n", delta.Seconds()) */
+	}
+	delta := container.FinishedAt.Sub(*last.Timestamp)
+	price, err := strconv.ParseFloat(*last.SpotPrice, 64)
+	if err != nil {
+		return 0, nil
+	}
+	//fmt.Printf("COST: %#v\n", price)
+	total += delta.Seconds() / 3600 * price
+
+	return total, err
+}
diff --git a/lib/costanalyzer/costanalyzer.go b/lib/costanalyzer/costanalyzer.go
index 4a48db1a8..e35c3d0cf 100644
--- a/lib/costanalyzer/costanalyzer.go
+++ b/lib/costanalyzer/costanalyzer.go
@@ -42,11 +42,13 @@ type nodeInfo struct {
 type consumption struct {
 	cost     float64
 	duration float64
+	savings  float64
 }
 
 func (c *consumption) Add(n consumption) {
 	c.cost += n.cost
 	c.duration += n.duration
+	c.savings += n.savings
 }
 
 type arrayFlags []string
@@ -199,7 +201,7 @@ func ensureDirectory(logger *logrus.Logger, dir string) (err error) {
 	return
 }
 
-func addContainerLine(logger *logrus.Logger, node nodeInfo, cr arvados.ContainerRequest, container arvados.Container) (string, consumption) {
+func addContainerLine(logger *logrus.Logger, node nodeInfo, cr arvados.ContainerRequest, container arvados.Container) (string, consumption, error) {
 	var csv string
 	var containerConsumption consumption
 	csv = cr.UUID + ","
@@ -229,10 +231,20 @@ func addContainerLine(logger *logrus.Logger, node nodeInfo, cr arvados.Container
 		price = node.Price
 		size = node.ProviderType
 	}
-	containerConsumption.cost = delta.Seconds() / 3600 * price
 	containerConsumption.duration = delta.Seconds()
-	csv += size + "," + fmt.Sprintf("%+v", node.Preemptible) + "," + strconv.FormatFloat(price, 'f', 8, 64) + "," + strconv.FormatFloat(containerConsumption.cost, 'f', 8, 64) + "\n"
-	return csv, containerConsumption
+	if !node.Preemptible {
+		containerConsumption.cost = delta.Seconds() / 3600 * price
+		csv += size + "," + fmt.Sprintf("%+v", node.Preemptible) + "," + strconv.FormatFloat(price, 'f', 8, 64) + "," + strconv.FormatFloat(containerConsumption.cost, 'f', 8, 64) + ",\n"
+	} else {
+		sum, err := CalculateAWSSpotPrice(container, size)
+		containerConsumption.cost = sum
+		containerConsumption.savings = delta.Seconds()/3600*price - sum
+		csv += size + "," + fmt.Sprintf("%+v", node.Preemptible) + ",," + strconv.FormatFloat(sum, 'f', 8, 64) + "," + strconv.FormatFloat(containerConsumption.savings, 'f', 8, 64) + "\n"
+		if err != nil {
+			return csv, containerConsumption, err
+		}
+	}
+	return csv, containerConsumption, nil
 }
 
 func loadCachedObject(logger *logrus.Logger, file string, uuid string, object interface{}) (reload bool) {
@@ -419,7 +431,7 @@ func generateCrInfo(logger *logrus.Logger, uuid string, arv *arvadosclient.Arvad
 
 	cost = make(map[string]consumption)
 
-	csv := "CR UUID,CR name,Container UUID,State,Started At,Finished At,Duration in seconds,Compute node type,Preemptible,Hourly node cost,Total cost\n"
+	csv := "CR UUID,CR name,Container UUID,State,Started At,Finished At,Duration in seconds,Compute node type,Preemptible,Hourly node cost,Total cost,Savings (if spot)\n"
 	var tmpCsv string
 	var total, tmpTotal consumption
 	logger.Debugf("Processing %s", uuid)
@@ -464,7 +476,10 @@ func generateCrInfo(logger *logrus.Logger, uuid string, arv *arvadosclient.Arvad
 		logger.Errorf("Skipping container request %s: error getting node %s: %s", cr.UUID, cr.UUID, err)
 		return nil, nil
 	}
-	tmpCsv, total = addContainerLine(logger, topNode, cr, container)
+	tmpCsv, total, err = addContainerLine(logger, topNode, cr, container)
+	if err != nil {
+		return nil, fmt.Errorf("error adding container line: %s", err.Error())
+	}
 	csv += tmpCsv
 	cost[container.UUID] = total
 
@@ -492,6 +507,10 @@ func generateCrInfo(logger *logrus.Logger, uuid string, arv *arvadosclient.Arvad
 			logger.Infof("... %d of %d", i+1, len(childCrs.Items))
 		default:
 		}
+		// We've already calculated this child
+		/*if _, err := os.Stat(resultsDir + "/" + crUUID + ".csv"); err == nil {
+			continue
+		}*/
 		node, err := getNode(arv, ac, kc, cr2)
 		if err != nil {
 			logger.Errorf("Skipping container request %s: error getting node %s: %s", cr2.UUID, cr2.UUID, err)
@@ -503,14 +522,17 @@ func generateCrInfo(logger *logrus.Logger, uuid string, arv *arvadosclient.Arvad
 		if err != nil {
 			return nil, fmt.Errorf("error loading object %s: %s", cr2.ContainerUUID, err)
 		}
-		tmpCsv, tmpTotal = addContainerLine(logger, node, cr2, c2)
+		tmpCsv, tmpTotal, err = addContainerLine(logger, node, cr2, c2)
+		if err != nil {
+			return nil, fmt.Errorf("error adding container line: %s", err.Error())
+		}
 		cost[cr2.ContainerUUID] = tmpTotal
 		csv += tmpCsv
 		total.Add(tmpTotal)
 	}
 	logger.Debug("Done collecting child containers")
 
-	csv += "TOTAL,,,,,," + strconv.FormatFloat(total.duration, 'f', 3, 64) + ",,,," + strconv.FormatFloat(total.cost, 'f', 2, 64) + "\n"
+	csv += "TOTAL,,,,,," + strconv.FormatFloat(total.duration, 'f', 3, 64) + ",,,," + strconv.FormatFloat(total.cost, 'f', 2, 64) + "," + strconv.FormatFloat(total.savings, 'f', 2, 64) + "\n"
 
 	if resultsDir != "" {
 		// Write the resulting CSV file
@@ -652,7 +674,7 @@ func (c *command) costAnalyzer(prog string, args []string, logger *logrus.Logger
 		total.Add(v)
 	}
 
-	csv += "TOTAL," + strconv.FormatFloat(total.duration, 'f', 3, 64) + "," + strconv.FormatFloat(total.cost, 'f', 2, 64) + "\n"
+	csv += "TOTAL," + strconv.FormatFloat(total.duration, 'f', 3, 64) + "," + strconv.FormatFloat(total.cost, 'f', 2, 64) + strconv.FormatFloat(total.savings, 'f', 2, 64) + "\n"
 
 	if c.resultsDir != "" {
 		// Write the resulting CSV file

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list