frostfs-node/pkg/local_object_storage/engine/container_stat.go
Dmitrii Stepanov 11ccf9fec9 [#9999] billing: Implement list containers.
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2024-06-24 16:39:43 +03:00

167 lines
4.5 KiB
Go

package engine
import (
"bytes"
"context"
"errors"
"sort"
"sync"
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/shard"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"golang.org/x/sync/errgroup"
)
type ContainerStatPrm struct {
ContainerID []cid.ID
Limit uint32
StartFromContainerID *cid.ID
}
type ContainerStatRes struct {
ContainerStats []ContainerStat
Partial bool
}
type ContainerStat struct {
ContainerID cid.ID
SizeLogic uint64
CountPhy, CountLogic, CountUser uint64
}
var errInvalidLimit = errors.New("limit must be greater than zero")
func (e *StorageEngine) ContainerStat(ctx context.Context, prm ContainerStatPrm) (*ContainerStatRes, error) {
if e.metrics != nil {
defer elapsed("ContainerStat", e.metrics.AddMethodDuration)()
}
ctx, span := tracing.StartSpanFromContext(ctx, "StorageEngine.ContainerStat",
trace.WithAttributes(
attribute.Int("container_ids", len(prm.ContainerID)),
attribute.Int64("limit", int64(prm.Limit)),
attribute.Bool("start_from_container_id", prm.StartFromContainerID != nil),
))
defer span.End()
if len(prm.ContainerID) == 0 && prm.Limit == 0 {
return nil, errInvalidLimit
}
var result *ContainerStatRes
err := e.execIfNotBlocked(func() error {
var sErr error
result, sErr = e.containerStat(ctx, prm)
return sErr
})
return result, err
}
func (e *StorageEngine) containerStat(ctx context.Context, prm ContainerStatPrm) (*ContainerStatRes, error) {
e.mtx.RLock()
defer e.mtx.RUnlock()
if len(prm.ContainerID) > 0 {
sort.Slice(prm.ContainerID, func(i, j int) bool {
return bytes.Compare(prm.ContainerID[i][:], prm.ContainerID[j][:]) < 0
})
}
shardResults, partial, err := e.collectShardContainerStats(ctx, prm)
if err != nil {
return nil, err
}
return &ContainerStatRes{
ContainerStats: e.mergeShardContainerStats(shardResults, int(prm.Limit)),
Partial: partial,
}, nil
}
func (e *StorageEngine) collectShardContainerStats(ctx context.Context, prm ContainerStatPrm) ([][]shard.ContainerStat, bool, error) {
if len(prm.ContainerID) > 0 {
sort.Slice(prm.ContainerID, func(i, j int) bool {
return bytes.Compare(prm.ContainerID[i][:], prm.ContainerID[j][:]) < 0
})
}
var shardResults [][]shard.ContainerStat
var shardErrors []error
var resultsGuard sync.Mutex
shPrm := shard.ContainerStatPrm{
ContainerID: prm.ContainerID,
Limit: prm.Limit,
StartFromContainerID: prm.StartFromContainerID,
}
eg, egCtx := errgroup.WithContext(ctx)
var shardsCount int
e.iterateOverUnsortedShards(func(hs hashedShard) (stop bool) {
shardsCount++
eg.Go(func() error {
s, err := hs.ContainerStat(egCtx, shPrm)
resultsGuard.Lock()
defer resultsGuard.Unlock()
if err != nil {
shardErrors = append(shardErrors, err)
return nil
}
if len(s) > 0 {
shardResults = append(shardResults, s)
}
return nil
})
return false
})
if err := eg.Wait(); err != nil {
return nil, false, err
}
if shardsCount == len(shardErrors) {
return nil, false, errors.Join(shardErrors...)
}
return shardResults, len(shardErrors) > 0, nil
}
func (e *StorageEngine) mergeShardContainerStats(shardResults [][]shard.ContainerStat, limit int) []ContainerStat {
var stats []ContainerStat
for len(stats) <= limit && len(shardResults) > 0 {
// shard results are sorted by container ID
sort.Slice(shardResults, func(i, j int) bool {
return bytes.Compare(shardResults[i][0].ContainerID[:], shardResults[j][0].ContainerID[:]) < 0
})
if len(stats) > 0 && stats[len(stats)-1].ContainerID == shardResults[0][0].ContainerID {
stats[len(stats)-1].SizeLogic += shardResults[0][0].SizeLogic
stats[len(stats)-1].CountPhy += shardResults[0][0].CountPhy
stats[len(stats)-1].CountLogic += shardResults[0][0].CountLogic
stats[len(stats)-1].CountUser += shardResults[0][0].CountUser
} else {
stats = append(stats, ContainerStat{
ContainerID: shardResults[0][0].ContainerID,
SizeLogic: shardResults[0][0].SizeLogic,
CountPhy: shardResults[0][0].CountPhy,
CountLogic: shardResults[0][0].CountLogic,
CountUser: shardResults[0][0].CountUser,
})
}
if len(shardResults[0]) == 1 { // last item for shard
shardResults = shardResults[1:]
} else {
shardResults[0] = shardResults[0][1:]
}
}
if len(stats) > limit {
stats = stats[:limit]
}
return stats
}