// CostForDuration returns the cost of running a host between the given start and end times func (cloudManager *EC2Manager) CostForDuration(h *host.Host, start, end time.Time) (float64, error) { // sanity check if end.Before(start) || util.IsZeroTime(start) || util.IsZeroTime(end) { return 0, fmt.Errorf("task timing data is malformed") } // grab instance details from EC2 ec2Handle := getUSEast(*cloudManager.awsCredentials) instance, err := getInstanceInfo(ec2Handle, h.Id) if err != nil { return 0, err } os := osLinux if strings.Contains(h.Distro.Arch, "windows") { os = osWindows } dur := end.Sub(start) region := azToRegion(instance.AvailabilityZone) iType := instance.InstanceType ebsCost, err := blockDeviceCosts(ec2Handle, instance.BlockDevices, dur) if err != nil { return 0, fmt.Errorf("calculating block device costs: %v", err) } hostCost, err := onDemandCost(&pkgOnDemandPriceFetcher, os, iType, region, dur) if err != nil { return 0, err } return hostCost + ebsCost, nil }
// CostForDuration computes the currency amount it costs to use the given host between a start and end time. // The Spot prices estimation takes both spot prices and EBS prices into account. Here's a breakdown: // // Spot prices are determined by a fluctuating price market. We set a bid price and get a host if the // "market" price is lower than that. We are billed by what the current spot price is, and then charged // the current spot price once our hour billing cycle is up, and so on. This calculator ONLY returns // the cost of the time used between the start and end times, it does not account for unused host time. // // EBS volumes are charged on a per-gigabyte-per-month rate for usage, rounded to the nearest hour. // There is no EBS price API, so we scrape it from Amazon's UI. This could unexpectedly break in the // future, but, so far, the JSON we are loading hasn't changed format in half a decade. EBS spending // for a single task ends up being virtually nothing compared to the machine price, but those fractions // of cents will add up over time. // // CostForDuration returns the total cost and any errors that occur. func (cloudManager *EC2SpotManager) CostForDuration(h *host.Host, start, end time.Time) (float64, error) { // sanity check if end.Before(start) || util.IsZeroTime(start) || util.IsZeroTime(end) { return 0, fmt.Errorf("task timing data is malformed") } // grab instance details from EC2 spotDetails, err := cloudManager.describeSpotRequest(h.Id) if err != nil { return 0, err } ec2Handle := getUSEast(*cloudManager.awsCredentials) instance, err := getInstanceInfo(ec2Handle, spotDetails.InstanceId) if err != nil { return 0, err } os := osLinux if strings.Contains(h.Distro.Arch, "windows") { os = osWindows } ebsCost, err := blockDeviceCosts(ec2Handle, instance.BlockDevices, end.Sub(start)) if err != nil { return 0, fmt.Errorf("calculating block device costs: %v", err) } spotCost, err := cloudManager.calculateSpotCost(instance, os, start, end) if err != nil { return 0, err } return spotCost + ebsCost, nil }
func TestbucketResource(t *testing.T) { Convey("With a start time and a bucket size of 10 and 10 buckets", t, func() { frameStart := time.Now() // 10 buckets * 10 bucket size = 100 frameEnd := frameStart.Add(time.Duration(100)) bucketSize := time.Duration(10) Convey("when resource start time is equal to end time should error", func() { buckets := make([]Bucket, 10) resourceStart := frameEnd resourceEnd := frameEnd.Add(time.Duration(10)) resource := ResourceInfo{ Start: resourceStart, End: resourceEnd, } buckets, err := bucketResource(resource, frameStart, frameEnd, bucketSize, buckets) So(err, ShouldNotBeNil) }) Convey("when resource start time is greater than end time should error", func() { buckets := make([]Bucket, 10) resourceStart := frameEnd.Add(time.Duration(10)) resourceEnd := frameEnd.Add(time.Duration(20)) resource := ResourceInfo{ Start: resourceStart, End: resourceEnd, } buckets, err := bucketResource(resource, frameStart, frameEnd, bucketSize, buckets) So(err, ShouldNotBeNil) }) Convey("when resource end time is equal to start time should error", func() { buckets := make([]Bucket, 10) resourceStart := frameStart.Add(time.Duration(-10)) resourceEnd := frameStart resource := ResourceInfo{ Start: resourceStart, End: resourceEnd, } buckets, err := bucketResource(resource, frameStart, frameEnd, bucketSize, buckets) So(err, ShouldNotBeNil) }) Convey("when resource end time is less than start time should error", func() { buckets := make([]Bucket, 10) resourceStart := frameStart.Add(time.Duration(-30)) resourceEnd := frameStart.Add(time.Duration(-10)) resource := ResourceInfo{ Start: resourceStart, End: resourceEnd, } buckets, err := bucketResource(resource, frameStart, frameEnd, bucketSize, buckets) So(err, ShouldNotBeNil) }) Convey("when resource end time is less than resource start time, should error", func() { buckets := make([]Bucket, 10) resourceStart := frameStart.Add(time.Duration(10)) resourceEnd := frameStart resource := ResourceInfo{ Start: resourceStart, End: resourceEnd, } buckets, err := bucketResource(resource, frameStart, frameEnd, bucketSize, buckets) So(err, ShouldNotBeNil) }) Convey("when resource start is zero, errors out", func() { buckets := make([]Bucket, 10) resourceStart := time.Time{} resourceEnd := frameStart.Add(time.Duration(1)) resource := ResourceInfo{ Start: resourceStart, End: resourceEnd, } buckets, err := bucketResource(resource, frameStart, frameEnd, bucketSize, buckets) So(err, ShouldNotBeNil) }) Convey("when the resource start and end time are in the same bucket, only one bucket has the difference", func() { buckets := make([]Bucket, 10) resourceStart := frameStart.Add(time.Duration(1)) resourceEnd := frameStart.Add(time.Duration(5)) resource := ResourceInfo{ Start: resourceStart, End: resourceEnd, } buckets, err := bucketResource(resource, frameStart, frameEnd, bucketSize, buckets) So(err, ShouldBeNil) So(buckets[0].TotalTime, ShouldEqual, time.Duration(4)) for i := 1; i < 10; i++ { So(buckets[i].TotalTime, ShouldEqual, 0) } }) Convey("when the resourceEnd is zero, there is no error", func() { buckets := make([]Bucket, 10) resourceStart := frameStart.Add(time.Duration(10)) resourceEnd := util.ZeroTime So(util.IsZeroTime(resourceEnd), ShouldBeTrue) resource := ResourceInfo{ Start: resourceStart, End: resourceEnd, } buckets, err := bucketResource(resource, frameStart, frameEnd, bucketSize, buckets) So(err, ShouldBeNil) So(buckets[0].TotalTime, ShouldEqual, 0) for i := 1; i < 10; i++ { So(buckets[i].TotalTime, ShouldEqual, 10) } }) }) }
// bucketResource buckets amounts of time based on a number of buckets and the size of them. // Given a resource with a start and end time, where the end >= start, // a time frame with a frameStart and frameEnd, where frameEnd >= frameStart, // a bucketSize that represents the amount of time each bucket holds // and a list of buckets that may already have time in them, // BucketResource will split the time and add the time the corresponds to a given buck to that bucket. func bucketResource(resource ResourceInfo, frameStart, frameEnd time.Time, bucketSize time.Duration, currentBuckets []Bucket) ([]Bucket, error) { start := resource.Start end := resource.End // double check so that there are no panics if start.After(frameEnd) || start.Equal(frameEnd) { return currentBuckets, fmt.Errorf("invalid resource start time %v that is after the time frame %v", start, frameEnd) } if util.IsZeroTime(start) { return currentBuckets, fmt.Errorf("start time is zero") } if !util.IsZeroTime(end) && (end.Before(frameStart) || end.Equal(frameStart)) { return currentBuckets, fmt.Errorf("invalid resource end time, %v that is before the time frame, %v", end, frameStart) } if !util.IsZeroTime(end) && end.Before(start) { return currentBuckets, fmt.Errorf("termination time, %v is before start time, %v and exists", end, start) } // if the times are equal then just return since nothing should be bucketed if end.Equal(start) { return currentBuckets, nil } // If the resource starts before the beginning of the frame, // the startBucket is the first one. The startOffset is the offset // of time from the beginning of the start bucket, so that is 0. startOffset := time.Duration(0) startBucket := time.Duration(0) if start.After(frameStart) { startOffset = start.Sub(frameStart) startBucket = startOffset / bucketSize } // If the resource ends after the end of the frame, the end bucket is the last bucket // the end offset is the entirety of that bucket. endBucket := time.Duration(len(currentBuckets) - 1) endOffset := bucketSize * (endBucket + 1) if !(util.IsZeroTime(end) || end.After(frameEnd) || end.Equal(frameEnd)) { endOffset = end.Sub(frameStart) endBucket = endOffset / bucketSize } // If the startBucket and the endBucket are the same, that means there is only one bucket. // The amount that goes in that bucket is the difference in the resources start time and end time. if startBucket == endBucket { currentBuckets[startBucket] = addBucketTime(endOffset-startOffset, resource, currentBuckets[startBucket]) return currentBuckets, nil } else { // add the difference between the startOffset and the amount of time that has passed in the start and end bucket // to the start and end buckets. currentBuckets[startBucket] = addBucketTime((startBucket+1)*bucketSize-startOffset, resource, currentBuckets[startBucket]) currentBuckets[endBucket] = addBucketTime(endOffset-endBucket*bucketSize, resource, currentBuckets[endBucket]) } for i := startBucket + 1; i < endBucket; i++ { currentBuckets[i] = addBucketTime(bucketSize, resource, currentBuckets[i]) } return currentBuckets, nil }