2019-04-02 19:21:18 +01:00
|
|
|
// Copyright (C) 2019 Storj Labs, Inc.
|
|
|
|
// See LICENSE for copying information.
|
|
|
|
|
|
|
|
package accounting
|
|
|
|
|
|
|
|
import (
|
2019-05-28 16:36:52 +01:00
|
|
|
"context"
|
|
|
|
"time"
|
|
|
|
|
2019-11-08 20:40:39 +00:00
|
|
|
"github.com/spacemonkeygo/monkit/v3"
|
2019-05-28 16:36:52 +01:00
|
|
|
"github.com/zeebo/errs"
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
|
2019-12-27 11:48:47 +00:00
|
|
|
"storj.io/common/memory"
|
2020-03-30 10:08:50 +01:00
|
|
|
"storj.io/common/uuid"
|
2020-06-02 00:19:10 +01:00
|
|
|
"storj.io/storj/storage"
|
2019-04-02 19:21:18 +01:00
|
|
|
)
|
|
|
|
|
2019-10-04 20:09:52 +01:00
|
|
|
var mon = monkit.Package()
|
|
|
|
|
2019-05-28 16:36:52 +01:00
|
|
|
var (
|
2020-08-11 15:50:01 +01:00
|
|
|
// ErrProjectUsage general error for project usage.
|
2019-05-28 16:36:52 +01:00
|
|
|
ErrProjectUsage = errs.Class("project usage error")
|
|
|
|
)
|
|
|
|
|
2019-11-15 14:27:44 +00:00
|
|
|
// Service is handling project usage related logic.
|
2019-09-10 14:24:16 +01:00
|
|
|
//
|
|
|
|
// architecture: Service
|
2019-11-15 14:27:44 +00:00
|
|
|
type Service struct {
|
2019-05-28 16:36:52 +01:00
|
|
|
projectAccountingDB ProjectAccounting
|
2019-10-16 17:50:29 +01:00
|
|
|
liveAccounting Cache
|
2020-05-12 14:01:15 +01:00
|
|
|
defaultMaxUsage memory.Size
|
|
|
|
defaultMaxBandwidth memory.Size
|
2020-06-02 00:19:10 +01:00
|
|
|
bandwidthCacheTTL time.Duration
|
2020-05-04 19:05:08 +01:00
|
|
|
nowFn func() time.Time
|
2019-05-28 16:36:52 +01:00
|
|
|
}
|
|
|
|
|
2019-11-15 14:27:44 +00:00
|
|
|
// NewService created new instance of project usage service.
|
2020-06-02 00:19:10 +01:00
|
|
|
func NewService(projectAccountingDB ProjectAccounting, liveAccounting Cache, defaultMaxUsage, defaultMaxBandwidth memory.Size, bandwidthCacheTTL time.Duration) *Service {
|
2019-11-15 14:27:44 +00:00
|
|
|
return &Service{
|
2019-05-28 16:36:52 +01:00
|
|
|
projectAccountingDB: projectAccountingDB,
|
|
|
|
liveAccounting: liveAccounting,
|
2020-05-12 14:01:15 +01:00
|
|
|
defaultMaxUsage: defaultMaxUsage,
|
|
|
|
defaultMaxBandwidth: defaultMaxBandwidth,
|
2020-06-02 00:19:10 +01:00
|
|
|
bandwidthCacheTTL: bandwidthCacheTTL,
|
2020-05-04 19:05:08 +01:00
|
|
|
nowFn: time.Now,
|
2019-05-28 16:36:52 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ExceedsBandwidthUsage returns true if the bandwidth usage limits have been exceeded
|
|
|
|
// for a project in the past month (30 days). The usage limit is (e.g 25GB) multiplied by the redundancy
|
|
|
|
// expansion factor, so that the uplinks have a raw limit.
|
2019-04-02 19:21:18 +01:00
|
|
|
// Ref: https://storjlabs.atlassian.net/browse/V3-1274
|
2020-08-28 16:10:02 +01:00
|
|
|
func (usage *Service) ExceedsBandwidthUsage(ctx context.Context, projectID uuid.UUID) (_ bool, limit memory.Size, err error) {
|
2019-06-04 12:36:27 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2019-05-28 16:36:52 +01:00
|
|
|
var group errgroup.Group
|
|
|
|
var bandwidthGetTotal int64
|
2020-06-02 00:19:10 +01:00
|
|
|
var bandwidthUsage int64
|
2019-05-28 16:36:52 +01:00
|
|
|
|
|
|
|
// TODO(michal): to reduce db load, consider using a cache to retrieve the project.UsageLimit value if needed
|
|
|
|
group.Go(func() error {
|
2019-12-12 12:58:15 +00:00
|
|
|
var err error
|
|
|
|
limit, err = usage.GetProjectBandwidthLimit(ctx, projectID)
|
2019-05-28 16:36:52 +01:00
|
|
|
return err
|
|
|
|
})
|
|
|
|
group.Go(func() error {
|
|
|
|
var err error
|
2020-06-02 00:19:10 +01:00
|
|
|
|
|
|
|
// Get the current bandwidth usage from cache.
|
|
|
|
bandwidthUsage, err = usage.liveAccounting.GetProjectBandwidthUsage(ctx, projectID, usage.nowFn())
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
// Verify If the cache key was not found
|
|
|
|
if storage.ErrKeyNotFound.Has(err) {
|
|
|
|
|
|
|
|
// Get current bandwidth value from database.
|
|
|
|
now := usage.nowFn()
|
|
|
|
bandwidthGetTotal, err = usage.GetProjectAllocatedBandwidth(ctx, projectID, now.Year(), now.Month())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create cache key with database value.
|
|
|
|
err = usage.liveAccounting.UpdateProjectBandwidthUsage(ctx, projectID, bandwidthGetTotal, usage.bandwidthCacheTTL, usage.nowFn())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
bandwidthUsage = bandwidthGetTotal
|
|
|
|
}
|
|
|
|
}
|
2019-05-28 16:36:52 +01:00
|
|
|
return err
|
|
|
|
})
|
|
|
|
|
|
|
|
err = group.Wait()
|
|
|
|
if err != nil {
|
|
|
|
return false, 0, ErrProjectUsage.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2020-06-02 00:19:10 +01:00
|
|
|
// Verify the bandwidth usage cache.
|
|
|
|
if bandwidthUsage >= limit.Int64() {
|
2019-05-28 16:36:52 +01:00
|
|
|
return true, limit, nil
|
2019-04-02 19:21:18 +01:00
|
|
|
}
|
|
|
|
|
2019-05-28 16:36:52 +01:00
|
|
|
return false, limit, nil
|
|
|
|
}
|
|
|
|
|
2019-10-31 17:27:38 +00:00
|
|
|
// ExceedsStorageUsage returns true if the storage usage for a project is currently over that project's limit.
|
2019-11-15 14:27:44 +00:00
|
|
|
func (usage *Service) ExceedsStorageUsage(ctx context.Context, projectID uuid.UUID) (_ bool, limit memory.Size, err error) {
|
2019-06-04 12:36:27 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2019-05-28 16:36:52 +01:00
|
|
|
var group errgroup.Group
|
2019-10-16 17:50:29 +01:00
|
|
|
var totalUsed int64
|
2019-05-28 16:36:52 +01:00
|
|
|
|
|
|
|
// TODO(michal): to reduce db load, consider using a cache to retrieve the project.UsageLimit value if needed
|
|
|
|
group.Go(func() error {
|
2019-12-12 12:58:15 +00:00
|
|
|
var err error
|
|
|
|
limit, err = usage.GetProjectStorageLimit(ctx, projectID)
|
2019-05-28 16:36:52 +01:00
|
|
|
return err
|
|
|
|
})
|
|
|
|
group.Go(func() error {
|
|
|
|
var err error
|
2019-12-12 12:58:15 +00:00
|
|
|
totalUsed, err = usage.GetProjectStorageTotals(ctx, projectID)
|
2019-05-28 16:36:52 +01:00
|
|
|
return err
|
|
|
|
})
|
2019-12-12 12:58:15 +00:00
|
|
|
|
2019-05-28 16:36:52 +01:00
|
|
|
err = group.Wait()
|
|
|
|
if err != nil {
|
|
|
|
return false, 0, ErrProjectUsage.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2019-10-31 17:27:38 +00:00
|
|
|
if totalUsed >= limit.Int64() {
|
2019-05-28 16:36:52 +01:00
|
|
|
return true, limit, nil
|
2019-04-02 19:21:18 +01:00
|
|
|
}
|
|
|
|
|
2019-05-28 16:36:52 +01:00
|
|
|
return false, limit, nil
|
|
|
|
}
|
|
|
|
|
2019-12-12 12:58:15 +00:00
|
|
|
// GetProjectStorageTotals returns total amount of storage used by project.
|
|
|
|
func (usage *Service) GetProjectStorageTotals(ctx context.Context, projectID uuid.UUID) (total int64, err error) {
|
|
|
|
defer mon.Task()(&ctx, projectID)(&err)
|
2019-06-04 12:36:27 +01:00
|
|
|
|
2019-10-31 17:27:38 +00:00
|
|
|
total, err = usage.liveAccounting.GetProjectStorageUsage(ctx, projectID)
|
|
|
|
|
|
|
|
return total, ErrProjectUsage.Wrap(err)
|
2019-05-28 16:36:52 +01:00
|
|
|
}
|
|
|
|
|
2019-12-12 12:58:15 +00:00
|
|
|
// GetProjectBandwidthTotals returns total amount of allocated bandwidth used for past 30 days.
|
|
|
|
func (usage *Service) GetProjectBandwidthTotals(ctx context.Context, projectID uuid.UUID) (_ int64, err error) {
|
|
|
|
defer mon.Task()(&ctx, projectID)(&err)
|
|
|
|
|
2020-03-11 22:00:58 +00:00
|
|
|
// from the beginning of the current month
|
2020-05-04 19:05:08 +01:00
|
|
|
year, month, _ := usage.nowFn().Date()
|
2020-03-11 22:00:58 +00:00
|
|
|
from := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
|
2019-12-12 12:58:15 +00:00
|
|
|
|
|
|
|
total, err := usage.projectAccountingDB.GetAllocatedBandwidthTotal(ctx, projectID, from)
|
|
|
|
return total, ErrProjectUsage.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2020-05-04 19:05:08 +01:00
|
|
|
// GetProjectAllocatedBandwidth returns project allocated bandwidth for the specified year and month.
|
|
|
|
func (usage *Service) GetProjectAllocatedBandwidth(ctx context.Context, projectID uuid.UUID, year int, month time.Month) (_ int64, err error) {
|
2020-05-01 14:24:12 +01:00
|
|
|
defer mon.Task()(&ctx, projectID)(&err)
|
|
|
|
|
2020-05-04 19:05:08 +01:00
|
|
|
total, err := usage.projectAccountingDB.GetProjectAllocatedBandwidth(ctx, projectID, year, month)
|
2020-05-01 14:24:12 +01:00
|
|
|
return total, ErrProjectUsage.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2019-12-12 12:58:15 +00:00
|
|
|
// GetProjectStorageLimit returns current project storage limit.
|
|
|
|
func (usage *Service) GetProjectStorageLimit(ctx context.Context, projectID uuid.UUID) (_ memory.Size, err error) {
|
|
|
|
defer mon.Task()(&ctx, projectID)(&err)
|
|
|
|
|
|
|
|
limit, err := usage.projectAccountingDB.GetProjectStorageLimit(ctx, projectID)
|
|
|
|
if err != nil {
|
|
|
|
return 0, ErrProjectUsage.Wrap(err)
|
|
|
|
}
|
2020-09-06 00:02:12 +01:00
|
|
|
if limit == nil {
|
2020-05-12 14:01:15 +01:00
|
|
|
return usage.defaultMaxUsage, nil
|
2019-12-12 12:58:15 +00:00
|
|
|
}
|
|
|
|
|
2020-09-06 00:02:12 +01:00
|
|
|
return memory.Size(*limit), nil
|
2019-12-12 12:58:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetProjectBandwidthLimit returns current project bandwidth limit.
|
|
|
|
func (usage *Service) GetProjectBandwidthLimit(ctx context.Context, projectID uuid.UUID) (_ memory.Size, err error) {
|
|
|
|
defer mon.Task()(&ctx, projectID)(&err)
|
|
|
|
|
|
|
|
limit, err := usage.projectAccountingDB.GetProjectBandwidthLimit(ctx, projectID)
|
|
|
|
if err != nil {
|
|
|
|
return 0, ErrProjectUsage.Wrap(err)
|
|
|
|
}
|
2020-09-06 00:02:12 +01:00
|
|
|
if limit == nil {
|
2020-05-12 14:01:15 +01:00
|
|
|
return usage.defaultMaxBandwidth, nil
|
2019-12-12 12:58:15 +00:00
|
|
|
}
|
|
|
|
|
2020-09-06 00:02:12 +01:00
|
|
|
return memory.Size(*limit), nil
|
2019-12-12 12:58:15 +00:00
|
|
|
}
|
|
|
|
|
2020-01-07 10:41:19 +00:00
|
|
|
// UpdateProjectLimits sets new value for project's bandwidth and storage limit.
|
|
|
|
func (usage *Service) UpdateProjectLimits(ctx context.Context, projectID uuid.UUID, limit memory.Size) (err error) {
|
|
|
|
defer mon.Task()(&ctx, projectID)(&err)
|
|
|
|
|
|
|
|
return ErrProjectUsage.Wrap(usage.projectAccountingDB.UpdateProjectUsageLimit(ctx, projectID, limit))
|
|
|
|
}
|
|
|
|
|
2020-06-02 00:19:10 +01:00
|
|
|
// GetProjectBandwidthUsage get the current bandwidth usage from cache.
|
|
|
|
func (usage *Service) GetProjectBandwidthUsage(ctx context.Context, projectID uuid.UUID) (currentUsed int64, err error) {
|
|
|
|
return usage.liveAccounting.GetProjectBandwidthUsage(ctx, projectID, usage.nowFn())
|
|
|
|
}
|
|
|
|
|
|
|
|
// UpdateProjectBandwidthUsage increments the bandwidth cache key for a specific project.
|
|
|
|
func (usage *Service) UpdateProjectBandwidthUsage(ctx context.Context, projectID uuid.UUID, increment int64) (err error) {
|
|
|
|
return usage.liveAccounting.UpdateProjectBandwidthUsage(ctx, projectID, increment, usage.bandwidthCacheTTL, usage.nowFn())
|
|
|
|
}
|
|
|
|
|
2019-05-28 16:36:52 +01:00
|
|
|
// AddProjectStorageUsage lets the live accounting know that the given
|
2019-10-31 17:27:38 +00:00
|
|
|
// project has just added spaceUsed bytes of storage (from the user's
|
|
|
|
// perspective; i.e. segment size).
|
|
|
|
func (usage *Service) AddProjectStorageUsage(ctx context.Context, projectID uuid.UUID, spaceUsed int64) (err error) {
|
|
|
|
defer mon.Task()(&ctx, projectID)(&err)
|
|
|
|
return usage.liveAccounting.AddProjectStorageUsage(ctx, projectID, spaceUsed)
|
2019-04-02 19:21:18 +01:00
|
|
|
}
|
2020-05-04 19:05:08 +01:00
|
|
|
|
|
|
|
// SetNow allows tests to have the Service act as if the current time is whatever they want.
|
|
|
|
func (usage *Service) SetNow(now func() time.Time) {
|
|
|
|
usage.nowFn = now
|
|
|
|
}
|