2019-01-24 20:15:10 +00:00
|
|
|
// Copyright (C) 2019 Storj Labs, Inc.
|
2018-11-08 16:18:28 +00:00
|
|
|
// See LICENSE for copying information.
|
|
|
|
|
2018-11-14 01:22:18 +00:00
|
|
|
package tally
|
2018-11-08 16:18:28 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/gogo/protobuf/proto"
|
2019-02-01 18:50:12 +00:00
|
|
|
"github.com/zeebo/errs"
|
2018-11-08 16:18:28 +00:00
|
|
|
"go.uber.org/zap"
|
2018-12-07 09:59:31 +00:00
|
|
|
|
2018-12-05 14:03:23 +00:00
|
|
|
"storj.io/storj/pkg/accounting"
|
2019-05-10 02:39:21 +01:00
|
|
|
"storj.io/storj/pkg/accounting/live"
|
2019-03-23 08:06:11 +00:00
|
|
|
"storj.io/storj/pkg/overlay"
|
2018-11-08 16:18:28 +00:00
|
|
|
"storj.io/storj/pkg/pb"
|
2018-11-29 18:39:27 +00:00
|
|
|
"storj.io/storj/pkg/storj"
|
2019-04-25 09:46:32 +01:00
|
|
|
"storj.io/storj/satellite/metainfo"
|
2018-11-08 16:18:28 +00:00
|
|
|
"storj.io/storj/storage"
|
|
|
|
)
|
|
|
|
|
2019-04-04 16:20:59 +01:00
|
|
|
// Config contains configurable values for the tally service
|
2019-01-23 19:58:44 +00:00
|
|
|
type Config struct {
|
2019-04-19 19:17:30 +01:00
|
|
|
Interval time.Duration `help:"how frequently the tally service should run" releaseDefault:"1h" devDefault:"30s"`
|
2018-11-08 16:18:28 +00:00
|
|
|
}
|
|
|
|
|
2019-04-04 16:20:59 +01:00
|
|
|
// Service is the tally service for data stored on each storage node
|
|
|
|
type Service struct {
|
2019-05-10 20:05:42 +01:00
|
|
|
logger *zap.Logger
|
|
|
|
metainfo *metainfo.Service
|
|
|
|
overlay *overlay.Cache
|
|
|
|
limit int
|
|
|
|
ticker *time.Ticker
|
|
|
|
storagenodeAccountingDB accounting.StoragenodeAccounting
|
|
|
|
projectAccountingDB accounting.ProjectAccounting
|
|
|
|
liveAccounting live.Service
|
2018-11-08 16:18:28 +00:00
|
|
|
}
|
|
|
|
|
2019-04-04 16:20:59 +01:00
|
|
|
// New creates a new tally Service
|
2019-05-10 20:05:42 +01:00
|
|
|
func New(logger *zap.Logger, sdb accounting.StoragenodeAccounting, pdb accounting.ProjectAccounting, liveAccounting live.Service, metainfo *metainfo.Service, overlay *overlay.Cache, limit int, interval time.Duration) *Service {
|
2019-04-04 16:20:59 +01:00
|
|
|
return &Service{
|
2019-05-10 20:05:42 +01:00
|
|
|
logger: logger,
|
|
|
|
metainfo: metainfo,
|
|
|
|
overlay: overlay,
|
|
|
|
limit: limit,
|
|
|
|
ticker: time.NewTicker(interval),
|
|
|
|
storagenodeAccountingDB: sdb,
|
|
|
|
projectAccountingDB: pdb,
|
|
|
|
liveAccounting: liveAccounting,
|
2018-12-05 14:03:23 +00:00
|
|
|
}
|
2018-11-08 16:18:28 +00:00
|
|
|
}
|
|
|
|
|
2019-04-04 16:20:59 +01:00
|
|
|
// Run the tally service loop
|
|
|
|
func (t *Service) Run(ctx context.Context) (err error) {
|
2018-11-08 16:18:28 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2019-06-04 12:36:27 +01:00
|
|
|
t.logger.Info("Tally service starting up")
|
2019-03-18 10:55:06 +00:00
|
|
|
|
2018-11-08 16:18:28 +00:00
|
|
|
for {
|
2019-02-01 18:50:12 +00:00
|
|
|
if err = t.Tally(ctx); err != nil {
|
|
|
|
t.logger.Error("Tally failed", zap.Error(err))
|
2018-11-08 16:18:28 +00:00
|
|
|
}
|
|
|
|
select {
|
|
|
|
case <-t.ticker.C: // wait for the next interval to happen
|
2019-01-23 19:58:44 +00:00
|
|
|
case <-ctx.Done(): // or the Tally is canceled via context
|
2018-11-08 16:18:28 +00:00
|
|
|
return ctx.Err()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-09 14:48:35 +01:00
|
|
|
// Tally calculates data-at-rest usage once
|
2019-06-04 12:36:27 +01:00
|
|
|
func (t *Service) Tally(ctx context.Context) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
2019-05-10 02:39:21 +01:00
|
|
|
// The live accounting store will only keep a delta to space used relative
|
|
|
|
// to the latest tally. Since a new tally is beginning, we will zero it out
|
|
|
|
// now. There is a window between this call and the point where the tally DB
|
|
|
|
// transaction starts, during which some changes in space usage may be
|
|
|
|
// double-counted (counted in the tally and also counted as a delta to
|
|
|
|
// the tally). If that happens, it will be fixed at the time of the next
|
|
|
|
// tally run.
|
|
|
|
t.liveAccounting.ResetTotals()
|
|
|
|
|
2019-04-04 16:20:59 +01:00
|
|
|
var errAtRest, errBucketInfo error
|
2019-04-09 14:48:35 +01:00
|
|
|
latestTally, nodeData, bucketData, err := t.CalculateAtRestData(ctx)
|
2019-02-01 18:50:12 +00:00
|
|
|
if err != nil {
|
|
|
|
errAtRest = errs.New("Query for data-at-rest failed : %v", err)
|
2019-04-01 14:42:17 +01:00
|
|
|
} else {
|
|
|
|
if len(nodeData) > 0 {
|
2019-05-10 20:05:42 +01:00
|
|
|
err = t.storagenodeAccountingDB.SaveTallies(ctx, latestTally, nodeData)
|
2019-04-01 14:42:17 +01:00
|
|
|
if err != nil {
|
|
|
|
errAtRest = errs.New("Saving storage node data-at-rest failed : %v", err)
|
|
|
|
}
|
|
|
|
}
|
2019-06-13 17:58:40 +01:00
|
|
|
|
2019-04-01 14:42:17 +01:00
|
|
|
if len(bucketData) > 0 {
|
2019-05-10 20:05:42 +01:00
|
|
|
_, err = t.projectAccountingDB.SaveTallies(ctx, latestTally, bucketData)
|
2019-04-01 14:42:17 +01:00
|
|
|
if err != nil {
|
|
|
|
errBucketInfo = errs.New("Saving bucket storage data failed")
|
|
|
|
}
|
2019-02-01 18:50:12 +00:00
|
|
|
}
|
|
|
|
}
|
2019-04-04 16:20:59 +01:00
|
|
|
return errs.Combine(errAtRest, errBucketInfo)
|
2019-02-01 18:50:12 +00:00
|
|
|
}
|
|
|
|
|
2019-04-25 09:46:32 +01:00
|
|
|
// CalculateAtRestData iterates through the pieces on metainfo and calculates
|
2019-04-01 14:42:17 +01:00
|
|
|
// the amount of at-rest data stored in each bucket and on each respective node
|
2019-04-09 14:48:35 +01:00
|
|
|
func (t *Service) CalculateAtRestData(ctx context.Context) (latestTally time.Time, nodeData map[storj.NodeID]float64, bucketTallies map[string]*accounting.BucketTally, err error) {
|
2018-11-08 16:18:28 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2019-01-16 19:30:33 +00:00
|
|
|
|
2019-05-10 20:05:42 +01:00
|
|
|
latestTally, err = t.storagenodeAccountingDB.LastTimestamp(ctx, accounting.LastAtRestTally)
|
2019-01-16 19:30:33 +00:00
|
|
|
if err != nil {
|
2019-04-01 14:42:17 +01:00
|
|
|
return latestTally, nodeData, bucketTallies, Error.Wrap(err)
|
2019-01-16 19:30:33 +00:00
|
|
|
}
|
2019-02-04 00:42:20 +00:00
|
|
|
nodeData = make(map[storj.NodeID]float64)
|
2019-04-01 14:42:17 +01:00
|
|
|
bucketTallies = make(map[string]*accounting.BucketTally)
|
2019-01-16 19:30:33 +00:00
|
|
|
|
2019-02-26 15:17:51 +00:00
|
|
|
var bucketCount int64
|
2019-06-13 17:58:40 +01:00
|
|
|
var totalTallies accounting.BucketTally
|
2019-02-26 15:17:51 +00:00
|
|
|
|
2019-06-05 15:23:10 +01:00
|
|
|
err = t.metainfo.Iterate(ctx, "", "", true, false,
|
|
|
|
func(ctx context.Context, it storage.Iterator) error {
|
2018-11-08 16:18:28 +00:00
|
|
|
var item storage.ListItem
|
2019-06-05 15:23:10 +01:00
|
|
|
for it.Next(ctx, &item) {
|
2019-02-26 15:17:51 +00:00
|
|
|
|
2018-11-08 16:18:28 +00:00
|
|
|
pointer := &pb.Pointer{}
|
|
|
|
err = proto.Unmarshal(item.Value, pointer)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
2019-02-26 15:17:51 +00:00
|
|
|
|
|
|
|
pathElements := storj.SplitPath(storj.Path(item.Key))
|
|
|
|
// check to make sure there are at least *4* path elements. the first three
|
|
|
|
// are project, segment, and bucket name, but we want to make sure we're talking
|
|
|
|
// about an actual object, and that there's an object name specified
|
2019-03-04 17:47:08 +00:00
|
|
|
|
|
|
|
// handle conditions with buckets with no files
|
|
|
|
if len(pathElements) == 3 {
|
|
|
|
bucketCount++
|
|
|
|
} else if len(pathElements) >= 4 {
|
2019-02-26 15:17:51 +00:00
|
|
|
project, segment, bucketName := pathElements[0], pathElements[1], pathElements[2]
|
2019-06-13 17:58:40 +01:00
|
|
|
|
2019-02-26 15:17:51 +00:00
|
|
|
bucketID := storj.JoinPaths(project, bucketName)
|
|
|
|
|
2019-06-13 17:58:40 +01:00
|
|
|
bucketTally := bucketTallies[bucketID]
|
|
|
|
if bucketTally == nil {
|
|
|
|
bucketTally = &accounting.BucketTally{}
|
|
|
|
bucketTally.ProjectID = []byte(project)
|
|
|
|
bucketTally.BucketName = []byte(bucketName)
|
|
|
|
|
|
|
|
bucketTallies[bucketID] = bucketTally
|
2019-02-26 15:17:51 +00:00
|
|
|
}
|
|
|
|
|
2019-06-13 17:58:40 +01:00
|
|
|
bucketTally.AddSegment(pointer, segment == "l")
|
2019-02-26 15:17:51 +00:00
|
|
|
}
|
|
|
|
|
2018-12-12 21:24:08 +00:00
|
|
|
remote := pointer.GetRemote()
|
|
|
|
if remote == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
pieces := remote.GetRemotePieces()
|
|
|
|
if pieces == nil {
|
|
|
|
t.logger.Debug("no pieces on remote segment")
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
segmentSize := pointer.GetSegmentSize()
|
|
|
|
redundancy := remote.GetRedundancy()
|
|
|
|
if redundancy == nil {
|
|
|
|
t.logger.Debug("no redundancy scheme present")
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
minReq := redundancy.GetMinReq()
|
|
|
|
if minReq <= 0 {
|
|
|
|
t.logger.Debug("pointer minReq must be an int greater than 0")
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
pieceSize := segmentSize / int64(minReq)
|
|
|
|
for _, piece := range pieces {
|
2019-01-16 19:30:33 +00:00
|
|
|
nodeData[piece.NodeId] += float64(pieceSize)
|
2018-11-08 16:18:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
)
|
2019-02-26 15:17:51 +00:00
|
|
|
if err != nil {
|
2019-04-01 14:42:17 +01:00
|
|
|
return latestTally, nodeData, bucketTallies, Error.Wrap(err)
|
2019-02-26 15:17:51 +00:00
|
|
|
}
|
|
|
|
|
2019-06-13 17:58:40 +01:00
|
|
|
for _, bucketTally := range bucketTallies {
|
|
|
|
bucketTally.Report("bucket")
|
|
|
|
totalTallies.Combine(bucketTally)
|
2018-12-18 17:18:42 +00:00
|
|
|
}
|
2019-06-13 17:58:40 +01:00
|
|
|
|
2019-04-01 14:42:17 +01:00
|
|
|
totalTallies.Report("total")
|
2019-02-26 15:17:51 +00:00
|
|
|
mon.IntVal("bucket_count").Observe(bucketCount)
|
|
|
|
|
2019-01-16 19:30:33 +00:00
|
|
|
//store byte hours, not just bytes
|
2019-02-01 18:50:12 +00:00
|
|
|
numHours := time.Now().Sub(latestTally).Hours()
|
|
|
|
if latestTally.IsZero() {
|
|
|
|
numHours = 1.0 //todo: something more considered?
|
2018-12-18 17:18:42 +00:00
|
|
|
}
|
2019-04-09 20:12:58 +01:00
|
|
|
latestTally = time.Now().UTC()
|
2019-04-29 18:46:38 +01:00
|
|
|
|
|
|
|
if len(nodeData) == 0 {
|
|
|
|
return latestTally, nodeData, bucketTallies, nil
|
|
|
|
}
|
2019-01-16 19:30:33 +00:00
|
|
|
for k := range nodeData {
|
2019-02-01 18:50:12 +00:00
|
|
|
nodeData[k] *= numHours //calculate byte hours
|
2018-12-18 17:18:42 +00:00
|
|
|
}
|
2019-04-01 14:42:17 +01:00
|
|
|
return latestTally, nodeData, bucketTallies, err
|
2019-02-01 18:50:12 +00:00
|
|
|
}
|