From 4a2c66fa06353ad22bf08e2c6118faee8a5b349c Mon Sep 17 00:00:00 2001 From: Jessica Grebenschikov Date: Wed, 9 Sep 2020 12:20:44 -0700 Subject: [PATCH] satellite/accounting: add cache for getting project storage and bw limits This PR adds the following items: 1) an in-memory read-only cache thats stores project limit info for projectIDs This cache is stored in-memory since this is expected to be a small amount of data. In this implementation we are only storing in the cache projects that have been accessed. Currently for the largest Satellite (eu-west) there is about 4500 total projects. So storing the storage limit (int64) and the bandwidth limit (int64), this would end up being about 200kb (including the 32 byte project ID) if all 4500 projectIDs were in the cache. So this all fits in memory for the time being. At some point it may not as usage grows, but that seems years out. The cache is a read only cache. When requests come in to upload/download a file, we will read from the cache what the current limits are for that project. If the cache does not contain the projectID, it will get the info from the database (satellitedb project table), then add it to the cache. The only time the values in the cache are modified is when either a) the project ID is not in the cache, or b) the item in the cache has expired (default 10mins), then the data gets refreshed out of the database. This occurs by default every 10 mins. This means that if we update the usage limits in the database, that change might not show up in the cache for 10 mins which mean it will not be reflected to limit end users uploading/downloading files for that time period.. Change-Id: I3fd7056cf963676009834fcbcf9c4a0922ca4a8f --- private/testplanet/satellite.go | 8 +- satellite/accounting/db.go | 8 + satellite/accounting/projectlimitcache.go | 124 ++++++++++++++ .../accounting/projectlimitcache_test.go | 161 ++++++++++++++++++ satellite/accounting/projectusage.go | 37 +--- satellite/accounting/projectusage_test.go | 1 - satellite/api.go | 15 +- .../consoleweb/consoleql/mutation_test.go | 4 +- .../consoleweb/consoleql/query_test.go | 4 +- satellite/core.go | 11 -- satellite/peer.go | 2 + satellite/satellitedb/dbx/satellitedb.dbx | 4 + satellite/satellitedb/dbx/satellitedb.dbx.go | 63 +++++++ satellite/satellitedb/projectaccounting.go | 17 ++ scripts/testdata/satellite-config.yaml.lock | 6 + 15 files changed, 418 insertions(+), 47 deletions(-) create mode 100644 satellite/accounting/projectlimitcache.go create mode 100644 satellite/accounting/projectlimitcache_test.go diff --git a/private/testplanet/satellite.go b/private/testplanet/satellite.go index e635ae217..dcd47e43a 100644 --- a/private/testplanet/satellite.go +++ b/private/testplanet/satellite.go @@ -152,6 +152,10 @@ type Satellite struct { Cache accounting.Cache } + ProjectLimits struct { + Cache *accounting.ProjectLimitCache + } + Mail struct { Service *mailservice.Service } @@ -726,12 +730,14 @@ func createNewSystem(log *zap.Logger, config satellite.Config, peer *satellite.C system.Accounting.Tally = peer.Accounting.Tally system.Accounting.Rollup = peer.Accounting.Rollup - system.Accounting.ProjectUsage = peer.Accounting.ProjectUsage + system.Accounting.ProjectUsage = api.Accounting.ProjectUsage system.Accounting.ReportedRollup = peer.Accounting.ReportedRollupChore system.Accounting.ProjectBWCleanup = peer.Accounting.ProjectBWCleanupChore system.LiveAccounting = peer.LiveAccounting + system.ProjectLimits.Cache = api.ProjectLimits.Cache + system.Marketing.Listener = api.Marketing.Listener system.Marketing.Endpoint = api.Marketing.Endpoint diff --git a/satellite/accounting/db.go b/satellite/accounting/db.go index 768af48b1..8048c42bd 100644 --- a/satellite/accounting/db.go +++ b/satellite/accounting/db.go @@ -76,6 +76,12 @@ type ProjectUsage struct { Before time.Time `json:"before"` } +// ProjectLimits contains the storage and bandwidth limits. +type ProjectLimits struct { + Usage *int64 + Bandwidth *int64 +} + // BucketUsage consist of total bucket usage for period. type BucketUsage struct { ProjectID uuid.UUID @@ -185,6 +191,8 @@ type ProjectAccounting interface { GetProjectStorageLimit(ctx context.Context, projectID uuid.UUID) (*int64, error) // GetProjectBandwidthLimit returns project bandwidth usage limit. GetProjectBandwidthLimit(ctx context.Context, projectID uuid.UUID) (*int64, error) + // GetProjectLimits returns current project limit for both storage and bandwidth. + GetProjectLimits(ctx context.Context, projectID uuid.UUID) (ProjectLimits, error) // GetProjectTotal returns project usage summary for specified period of time. GetProjectTotal(ctx context.Context, projectID uuid.UUID, since, before time.Time) (*ProjectUsage, error) // GetBucketUsageRollups returns usage rollup per each bucket for specified period of time. diff --git a/satellite/accounting/projectlimitcache.go b/satellite/accounting/projectlimitcache.go new file mode 100644 index 000000000..a38480278 --- /dev/null +++ b/satellite/accounting/projectlimitcache.go @@ -0,0 +1,124 @@ +// Copyright (C) 2019 Storj Labs, Inc. +// See LICENSE for copying information. + +package accounting + +import ( + "context" + "time" + + "github.com/zeebo/errs" + + "storj.io/common/memory" + "storj.io/common/uuid" + lrucache "storj.io/storj/pkg/cache" +) + +var ( + // ErrProjectLimitType error for project limit type. + ErrProjectLimitType = errs.Class("project limit type error") + // ErrGetProjectLimit error for getting project limits from database. + ErrGetProjectLimit = errs.Class("get project limits error") + // ErrGetProjectLimitCache error for getting project limits from cache. + ErrGetProjectLimitCache = errs.Class("get project limits from cache error") +) + +// ProjectLimitDB stores information about projects limits for storage and bandwidth limits. +// +// architecture: Database +type ProjectLimitDB interface { + // GetProjectLimits returns current project limit for both storage and bandwidth. + GetProjectLimits(ctx context.Context, projectID uuid.UUID) (ProjectLimits, error) +} + +// ProjectLimitConfig is a configuration struct for project limit. +type ProjectLimitConfig struct { + CacheCapacity int `help:"number of projects to cache." releaseDefault:"10000" devDefault:"100"` + CacheExpiration time.Duration `help:"how long to cache the project limits." releaseDefault:"10m" devDefault:"30s"` +} + +// ProjectLimitCache stores the values for both storage usage limit and bandwidth limit for +// each project ID if they differ from the default limits. +type ProjectLimitCache struct { + projectLimitDB ProjectLimitDB + defaultMaxUsage memory.Size + defaultMaxBandwidth memory.Size + + state *lrucache.ExpiringLRU +} + +// NewProjectLimitCache creates a new project limit cache to store the project limits for each project ID. +func NewProjectLimitCache(db ProjectLimitDB, defaultMaxUsage, defaultMaxBandwidth memory.Size, config ProjectLimitConfig) *ProjectLimitCache { + return &ProjectLimitCache{ + projectLimitDB: db, + defaultMaxUsage: defaultMaxUsage, + defaultMaxBandwidth: defaultMaxBandwidth, + state: lrucache.New(lrucache.Options{ + Capacity: config.CacheCapacity, + Expiration: config.CacheExpiration, + }), + } +} + +// GetProjectLimits returns current project limit for both storage and bandwidth. +func (c *ProjectLimitCache) GetProjectLimits(ctx context.Context, projectID uuid.UUID) (_ ProjectLimits, err error) { + defer mon.Task()(&ctx, projectID)(&err) + + projectLimits, err := c.projectLimitDB.GetProjectLimits(ctx, projectID) + if err != nil { + return ProjectLimits{}, ErrGetProjectLimit.Wrap(err) + } + if projectLimits.Bandwidth == nil { + defaultBandwidth := c.defaultMaxBandwidth.Int64() + projectLimits.Bandwidth = &defaultBandwidth + } + if projectLimits.Usage == nil { + defaultUsage := c.defaultMaxUsage.Int64() + projectLimits.Usage = &defaultUsage + } + + return projectLimits, nil +} + +// Get returns the storage usage limit for a project ID. +func (c *ProjectLimitCache) Get(ctx context.Context, projectID uuid.UUID) (ProjectLimits, error) { + fn := func() (interface{}, error) { + return c.GetProjectLimits(ctx, projectID) + } + projectLimits, err := c.state.Get(projectID.String(), fn) + if err != nil { + return ProjectLimits{}, ErrGetProjectLimitCache.Wrap(err) + } + limits, ok := projectLimits.(ProjectLimits) + if !ok { + return ProjectLimits{}, ErrProjectLimitType.New("cache Get error") + } + return limits, nil +} + +// GetProjectStorageLimit returns the storage usage limit for a project ID. +func (c *ProjectLimitCache) GetProjectStorageLimit(ctx context.Context, projectID uuid.UUID) (_ memory.Size, err error) { + defer mon.Task()(&ctx)(&err) + projectLimits, err := c.Get(ctx, projectID) + if err != nil { + return 0, err + } + if projectLimits.Usage == nil { + return c.defaultMaxUsage, nil + } + return memory.Size(*projectLimits.Usage), nil + +} + +// GetProjectBandwidthLimit return the bandwidth usage limit for a project ID. +func (c *ProjectLimitCache) GetProjectBandwidthLimit(ctx context.Context, projectID uuid.UUID) (_ memory.Size, err error) { + defer mon.Task()(&ctx)(&err) + projectLimits, err := c.Get(ctx, projectID) + if err != nil { + return 0, err + } + if projectLimits.Bandwidth == nil { + return c.defaultMaxBandwidth, nil + } + return memory.Size(*projectLimits.Bandwidth), nil +} diff --git a/satellite/accounting/projectlimitcache_test.go b/satellite/accounting/projectlimitcache_test.go new file mode 100644 index 000000000..62a3fc819 --- /dev/null +++ b/satellite/accounting/projectlimitcache_test.go @@ -0,0 +1,161 @@ +// Copyright (C) 2020 Storj Labs, Inc. +// See LICENSE for copying information. + +package accounting_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "storj.io/common/memory" + "storj.io/common/testcontext" + "storj.io/common/testrand" + "storj.io/common/uuid" + "storj.io/storj/private/testplanet" + "storj.io/storj/satellite" + "storj.io/storj/satellite/accounting" + "storj.io/storj/satellite/console" + "storj.io/storj/satellite/satellitedb/satellitedbtest" +) + +type mockDB struct { + callCount int +} + +func (mdb *mockDB) GetProjectLimits(ctx context.Context, projectID uuid.UUID) (accounting.ProjectLimits, error) { + mdb.callCount++ + return accounting.ProjectLimits{}, nil +} +func TestProjectLimitCacheCallCount(t *testing.T) { + satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) { + mdb := mockDB{} + projectLimitCache := accounting.NewProjectLimitCache(&mdb, 0, 0, accounting.ProjectLimitConfig{CacheCapacity: 100}) + + testProject, err := db.Console().Projects().Insert(ctx, &console.Project{Name: "test", OwnerID: testrand.UUID()}) + require.NoError(t, err) + + const expectedCallCount = 1 + + _, err = projectLimitCache.GetProjectBandwidthLimit(ctx, testProject.ID) + require.NoError(t, err) + // if the data isn't in the cache we call into the database to get it + require.Equal(t, expectedCallCount, mdb.callCount) + + _, err = projectLimitCache.GetProjectBandwidthLimit(ctx, testProject.ID) + require.NoError(t, err) + // call count should still be 1 since the data is in the cache and we don't need + // to get it from the db + require.Equal(t, expectedCallCount, mdb.callCount) + }) +} + +func TestProjectLimitCache(t *testing.T) { + testplanet.Run(t, testplanet.Config{ + SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0, + }, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) { + saPeer := planet.Satellites[0] + projectUsageSvc := saPeer.Accounting.ProjectUsage + accountingDB := saPeer.DB.ProjectAccounting() + projectLimitCache := saPeer.ProjectLimits.Cache + + testProject, err := saPeer.DB.Console().Projects().Insert(ctx, &console.Project{Name: "test", OwnerID: testrand.UUID()}) + require.NoError(t, err) + + const ( + dbDefaultLimits = 50000000000 + errorLimit = 0 + expectedUsageLimit = 1 + expectedBandwidthLimit = 2 + ) + + t.Run("project ID doesn't exist", func(t *testing.T) { + projectID := testrand.UUID() + actualStorageLimitFromDB, err := accountingDB.GetProjectStorageLimit(ctx, projectID) + assert.Error(t, err) + assert.Nil(t, actualStorageLimitFromDB) + + actualLimitsFromDB, err := accountingDB.GetProjectLimits(ctx, projectID) + assert.Error(t, err) + assert.Equal(t, accounting.ProjectLimits{}, actualLimitsFromDB) + + actualStorageLimitFromCache, err := projectLimitCache.GetProjectStorageLimit(ctx, projectID) + assert.Error(t, err) + assert.Equal(t, memory.Size(errorLimit), actualStorageLimitFromCache) + + actualStorageLimitFromSvc, err := projectUsageSvc.GetProjectStorageLimit(ctx, projectID) + assert.Error(t, err) + assert.Equal(t, memory.Size(errorLimit), actualStorageLimitFromSvc) + }) + + t.Run("default limits", func(t *testing.T) { + actualStorageLimitFromDB, err := accountingDB.GetProjectStorageLimit(ctx, testProject.ID) + assert.NoError(t, err) + assert.Equal(t, int64(dbDefaultLimits), *actualStorageLimitFromDB) + + actualLimitsFromDB, err := accountingDB.GetProjectLimits(ctx, testProject.ID) + assert.NoError(t, err) + defaultLimits := int64(dbDefaultLimits) + assert.Equal(t, accounting.ProjectLimits{Usage: &defaultLimits, Bandwidth: &defaultLimits}, actualLimitsFromDB) + + actualStorageLimitFromCache, err := projectLimitCache.GetProjectStorageLimit(ctx, testProject.ID) + assert.NoError(t, err) + assert.Equal(t, memory.Size(dbDefaultLimits), actualStorageLimitFromCache) + + actualStorageLimitFromSvc, err := projectUsageSvc.GetProjectStorageLimit(ctx, testProject.ID) + assert.NoError(t, err) + assert.Equal(t, memory.Size(dbDefaultLimits), actualStorageLimitFromSvc) + + actualBandwidthLimitFromDB, err := accountingDB.GetProjectBandwidthLimit(ctx, testProject.ID) + assert.NoError(t, err) + assert.Equal(t, int64(dbDefaultLimits), *actualBandwidthLimitFromDB) + + actualBandwidthLimitFromCache, err := projectLimitCache.GetProjectBandwidthLimit(ctx, testProject.ID) + assert.NoError(t, err) + assert.Equal(t, memory.Size(dbDefaultLimits), actualBandwidthLimitFromCache) + + actualBandwidthLimitFromSvc, err := projectUsageSvc.GetProjectBandwidthLimit(ctx, testProject.ID) + assert.NoError(t, err) + assert.Equal(t, memory.Size(dbDefaultLimits), actualBandwidthLimitFromSvc) + }) + + t.Run("update limits in the database", func(t *testing.T) { + err = accountingDB.UpdateProjectUsageLimit(ctx, testProject.ID, expectedUsageLimit) + require.NoError(t, err) + err = accountingDB.UpdateProjectBandwidthLimit(ctx, testProject.ID, expectedBandwidthLimit) + require.NoError(t, err) + + actualStorageLimitFromDB, err := accountingDB.GetProjectStorageLimit(ctx, testProject.ID) + assert.NoError(t, err) + require.Equal(t, int64(expectedUsageLimit), *actualStorageLimitFromDB) + + actualLimitsFromDB, err := accountingDB.GetProjectLimits(ctx, testProject.ID) + assert.NoError(t, err) + usageLimits := int64(expectedUsageLimit) + bwLimits := int64(expectedBandwidthLimit) + assert.Equal(t, accounting.ProjectLimits{Usage: &usageLimits, Bandwidth: &bwLimits}, actualLimitsFromDB) + + actualStorageLimitFromCache, err := projectLimitCache.GetProjectStorageLimit(ctx, testProject.ID) + assert.NoError(t, err) + require.Equal(t, memory.Size(expectedUsageLimit), actualStorageLimitFromCache) + + actualStorageLimitFromSvc, err := projectUsageSvc.GetProjectStorageLimit(ctx, testProject.ID) + assert.NoError(t, err) + require.Equal(t, memory.Size(expectedUsageLimit), actualStorageLimitFromSvc) + + actualBandwidthLimitFromDB, err := accountingDB.GetProjectBandwidthLimit(ctx, testProject.ID) + require.NoError(t, err) + require.Equal(t, int64(expectedBandwidthLimit), *actualBandwidthLimitFromDB) + + actualBandwidthLimitFromCache, err := projectLimitCache.GetProjectBandwidthLimit(ctx, testProject.ID) + assert.NoError(t, err) + require.Equal(t, memory.Size(expectedBandwidthLimit), actualBandwidthLimitFromCache) + + actualBandwidthLimitFromSvc, err := projectUsageSvc.GetProjectBandwidthLimit(ctx, testProject.ID) + assert.NoError(t, err) + require.Equal(t, memory.Size(expectedBandwidthLimit), actualBandwidthLimitFromSvc) + }) + }) +} diff --git a/satellite/accounting/projectusage.go b/satellite/accounting/projectusage.go index cd66463c8..6f9414928 100644 --- a/satellite/accounting/projectusage.go +++ b/satellite/accounting/projectusage.go @@ -29,19 +29,17 @@ var ( type Service struct { projectAccountingDB ProjectAccounting liveAccounting Cache - defaultMaxUsage memory.Size - defaultMaxBandwidth memory.Size + projectLimitCache *ProjectLimitCache bandwidthCacheTTL time.Duration nowFn func() time.Time } // NewService created new instance of project usage service. -func NewService(projectAccountingDB ProjectAccounting, liveAccounting Cache, defaultMaxUsage, defaultMaxBandwidth memory.Size, bandwidthCacheTTL time.Duration) *Service { +func NewService(projectAccountingDB ProjectAccounting, liveAccounting Cache, limitCache *ProjectLimitCache, bandwidthCacheTTL time.Duration) *Service { return &Service{ projectAccountingDB: projectAccountingDB, liveAccounting: liveAccounting, - defaultMaxUsage: defaultMaxUsage, - defaultMaxBandwidth: defaultMaxBandwidth, + projectLimitCache: limitCache, bandwidthCacheTTL: bandwidthCacheTTL, nowFn: time.Now, } @@ -50,7 +48,6 @@ func NewService(projectAccountingDB ProjectAccounting, liveAccounting Cache, def // 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. -// Ref: https://storjlabs.atlassian.net/browse/V3-1274 func (usage *Service) ExceedsBandwidthUsage(ctx context.Context, projectID uuid.UUID) (_ bool, limit memory.Size, err error) { defer mon.Task()(&ctx)(&err) @@ -58,10 +55,9 @@ func (usage *Service) ExceedsBandwidthUsage(ctx context.Context, projectID uuid. var bandwidthGetTotal int64 var bandwidthUsage int64 - // TODO(michal): to reduce db load, consider using a cache to retrieve the project.UsageLimit value if needed group.Go(func() error { var err error - limit, err = usage.GetProjectBandwidthLimit(ctx, projectID) + limit, err = usage.projectLimitCache.GetProjectBandwidthLimit(ctx, projectID) return err }) group.Go(func() error { @@ -113,10 +109,9 @@ func (usage *Service) ExceedsStorageUsage(ctx context.Context, projectID uuid.UU var group errgroup.Group var totalUsed int64 - // TODO(michal): to reduce db load, consider using a cache to retrieve the project.UsageLimit value if needed group.Go(func() error { var err error - limit, err = usage.GetProjectStorageLimit(ctx, projectID) + limit, err = usage.projectLimitCache.GetProjectStorageLimit(ctx, projectID) return err }) group.Go(func() error { @@ -169,31 +164,13 @@ func (usage *Service) GetProjectAllocatedBandwidth(ctx context.Context, projectI // 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) - } - if limit == nil { - return usage.defaultMaxUsage, nil - } - - return memory.Size(*limit), nil + return usage.projectLimitCache.GetProjectStorageLimit(ctx, projectID) } // 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) - } - if limit == nil { - return usage.defaultMaxBandwidth, nil - } - - return memory.Size(*limit), nil + return usage.projectLimitCache.GetProjectBandwidthLimit(ctx, projectID) } // UpdateProjectLimits sets new value for project's bandwidth and storage limit. diff --git a/satellite/accounting/projectusage_test.go b/satellite/accounting/projectusage_test.go index 2d940594f..714105012 100644 --- a/satellite/accounting/projectusage_test.go +++ b/satellite/accounting/projectusage_test.go @@ -328,7 +328,6 @@ func TestProjectUsageCustomLimit(t *testing.T) { project := projects[0] // set custom usage limit for project expectedLimit := memory.Size(memory.GiB.Int64() * 10) - err = acctDB.UpdateProjectUsageLimit(ctx, project.ID, expectedLimit) require.NoError(t, err) diff --git a/satellite/api.go b/satellite/api.go index a776b302c..d9e27ae81 100644 --- a/satellite/api.go +++ b/satellite/api.go @@ -118,6 +118,10 @@ type API struct { Cache accounting.Cache } + ProjectLimits struct { + Cache *accounting.ProjectLimitCache + } + Mail struct { Service *mailservice.Service } @@ -293,12 +297,19 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB, peer.LiveAccounting.Cache = liveAccounting } + { // setup project limits + peer.ProjectLimits.Cache = accounting.NewProjectLimitCache(peer.DB.ProjectAccounting(), + config.Metainfo.ProjectLimits.DefaultMaxUsage, + config.Metainfo.ProjectLimits.DefaultMaxBandwidth, + config.ProjectLimit, + ) + } + { // setup accounting project usage peer.Accounting.ProjectUsage = accounting.NewService( peer.DB.ProjectAccounting(), peer.LiveAccounting.Cache, - config.Metainfo.ProjectLimits.DefaultMaxUsage, - config.Metainfo.ProjectLimits.DefaultMaxBandwidth, + peer.ProjectLimits.Cache, config.LiveAccounting.BandwidthCacheTTL, ) } diff --git a/satellite/console/consoleweb/consoleql/mutation_test.go b/satellite/console/consoleweb/consoleql/mutation_test.go index e9b9840d3..4befd1afe 100644 --- a/satellite/console/consoleweb/consoleql/mutation_test.go +++ b/satellite/console/consoleweb/consoleql/mutation_test.go @@ -67,7 +67,9 @@ func TestGrapqhlMutation(t *testing.T) { cache, err := live.NewCache(log.Named("cache"), live.Config{StorageBackend: "redis://" + redis.Addr() + "?db=0"}) require.NoError(t, err) - projectUsage := accounting.NewService(db.ProjectAccounting(), cache, 0, 0, 5*time.Minute) + projectLimitCache := accounting.NewProjectLimitCache(db.ProjectAccounting(), 0, 0, accounting.ProjectLimitConfig{CacheCapacity: 100}) + + projectUsage := accounting.NewService(db.ProjectAccounting(), cache, projectLimitCache, 5*time.Minute) // TODO maybe switch this test to testplanet to avoid defining config and Stripe service pc := paymentsconfig.Config{ diff --git a/satellite/console/consoleweb/consoleql/query_test.go b/satellite/console/consoleweb/consoleql/query_test.go index cc0ff7232..c272179ba 100644 --- a/satellite/console/consoleweb/consoleql/query_test.go +++ b/satellite/console/consoleweb/consoleql/query_test.go @@ -51,7 +51,9 @@ func TestGraphqlQuery(t *testing.T) { cache, err := live.NewCache(log.Named("cache"), live.Config{StorageBackend: "redis://" + redis.Addr() + "?db=0"}) require.NoError(t, err) - projectUsage := accounting.NewService(db.ProjectAccounting(), cache, 0, 0, 5*time.Minute) + projectLimitCache := accounting.NewProjectLimitCache(db.ProjectAccounting(), 0, 0, accounting.ProjectLimitConfig{CacheCapacity: 100}) + + projectUsage := accounting.NewService(db.ProjectAccounting(), cache, projectLimitCache, 5*time.Minute) // TODO maybe switch this test to testplanet to avoid defining config and Stripe service pc := paymentsconfig.Config{ diff --git a/satellite/core.go b/satellite/core.go index a108831e0..475b577a2 100644 --- a/satellite/core.go +++ b/satellite/core.go @@ -117,7 +117,6 @@ type Core struct { Accounting struct { Tally *tally.Service Rollup *rollup.Service - ProjectUsage *accounting.Service ReportedRollupChore *reportedrollup.Chore ProjectBWCleanupChore *projectbwcleanup.Chore } @@ -245,16 +244,6 @@ func New(log *zap.Logger, full *identity.FullIdentity, db DB, peer.LiveAccounting.Cache = liveAccounting } - { // setup accounting project usage - peer.Accounting.ProjectUsage = accounting.NewService( - peer.DB.ProjectAccounting(), - peer.LiveAccounting.Cache, - config.Metainfo.ProjectLimits.DefaultMaxUsage, - config.Metainfo.ProjectLimits.DefaultMaxBandwidth, - config.LiveAccounting.BandwidthCacheTTL, - ) - } - { // setup orders peer.Orders.DB = rollupsWriteCache peer.Orders.Chore = orders.NewChore(log.Named("orders:chore"), rollupsWriteCache, config.Orders) diff --git a/satellite/peer.go b/satellite/peer.go index 85784879b..3543c34ea 100644 --- a/satellite/peer.go +++ b/satellite/peer.go @@ -159,4 +159,6 @@ type Config struct { Downtime downtime.Config Compensation compensation.Config + + ProjectLimit accounting.ProjectLimitConfig } diff --git a/satellite/satellitedb/dbx/satellitedb.dbx b/satellite/satellitedb/dbx/satellitedb.dbx index 203761ef0..78f118b15 100644 --- a/satellite/satellitedb/dbx/satellitedb.dbx +++ b/satellite/satellitedb/dbx/satellitedb.dbx @@ -350,6 +350,10 @@ read one ( select project.max_buckets where project.id = ? ) +read one ( + select project.bandwidth_limit project.usage_limit + where project.id = ? +) read all ( select project diff --git a/satellite/satellitedb/dbx/satellitedb.dbx.go b/satellite/satellitedb/dbx/satellitedb.dbx.go index 661bb21ff..19ba0d6fd 100644 --- a/satellite/satellitedb/dbx/satellitedb.dbx.go +++ b/satellite/satellitedb/dbx/satellitedb.dbx.go @@ -8715,6 +8715,11 @@ type BandwidthLimit_Row struct { BandwidthLimit *int64 } +type BandwidthLimit_UsageLimit_Row struct { + BandwidthLimit int64 + UsageLimit int64 +} + type BucketId_Row struct { BucketId []byte } @@ -10850,6 +10855,28 @@ func (obj *pgxImpl) Get_Project_MaxBuckets_By_Id(ctx context.Context, } +func (obj *pgxImpl) Get_Project_BandwidthLimit_Project_UsageLimit_By_Id(ctx context.Context, + project_id Project_Id_Field) ( + row *BandwidthLimit_UsageLimit_Row, err error) { + defer mon.Task()(&ctx)(&err) + + var __embed_stmt = __sqlbundle_Literal("SELECT projects.bandwidth_limit, projects.usage_limit FROM projects WHERE projects.id = ?") + + var __values []interface{} + __values = append(__values, project_id.value()) + + var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt) + obj.logStmt(__stmt, __values...) + + row = &BandwidthLimit_UsageLimit_Row{} + err = obj.driver.QueryRowContext(ctx, __stmt, __values...).Scan(&row.BandwidthLimit, &row.UsageLimit) + if err != nil { + return (*BandwidthLimit_UsageLimit_Row)(nil), obj.makeErr(err) + } + return row, nil + +} + func (obj *pgxImpl) All_Project(ctx context.Context) ( rows []*Project, err error) { defer mon.Task()(&ctx)(&err) @@ -17117,6 +17144,28 @@ func (obj *pgxcockroachImpl) Get_Project_MaxBuckets_By_Id(ctx context.Context, } +func (obj *pgxcockroachImpl) Get_Project_BandwidthLimit_Project_UsageLimit_By_Id(ctx context.Context, + project_id Project_Id_Field) ( + row *BandwidthLimit_UsageLimit_Row, err error) { + defer mon.Task()(&ctx)(&err) + + var __embed_stmt = __sqlbundle_Literal("SELECT projects.bandwidth_limit, projects.usage_limit FROM projects WHERE projects.id = ?") + + var __values []interface{} + __values = append(__values, project_id.value()) + + var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt) + obj.logStmt(__stmt, __values...) + + row = &BandwidthLimit_UsageLimit_Row{} + err = obj.driver.QueryRowContext(ctx, __stmt, __values...).Scan(&row.BandwidthLimit, &row.UsageLimit) + if err != nil { + return (*BandwidthLimit_UsageLimit_Row)(nil), obj.makeErr(err) + } + return row, nil + +} + func (obj *pgxcockroachImpl) All_Project(ctx context.Context) ( rows []*Project, err error) { defer mon.Task()(&ctx)(&err) @@ -22585,6 +22634,16 @@ func (rx *Rx) Get_Project_BandwidthLimit_By_Id(ctx context.Context, return tx.Get_Project_BandwidthLimit_By_Id(ctx, project_id) } +func (rx *Rx) Get_Project_BandwidthLimit_Project_UsageLimit_By_Id(ctx context.Context, + project_id Project_Id_Field) ( + row *BandwidthLimit_UsageLimit_Row, err error) { + var tx *Tx + if tx, err = rx.getTx(ctx); err != nil { + return + } + return tx.Get_Project_BandwidthLimit_Project_UsageLimit_By_Id(ctx, project_id) +} + func (rx *Rx) Get_Project_By_Id(ctx context.Context, project_id Project_Id_Field) ( project *Project, err error) { @@ -23750,6 +23809,10 @@ type Methods interface { project_id Project_Id_Field) ( row *BandwidthLimit_Row, err error) + Get_Project_BandwidthLimit_Project_UsageLimit_By_Id(ctx context.Context, + project_id Project_Id_Field) ( + row *BandwidthLimit_UsageLimit_Row, err error) + Get_Project_By_Id(ctx context.Context, project_id Project_Id_Field) ( project *Project, err error) diff --git a/satellite/satellitedb/projectaccounting.go b/satellite/satellitedb/projectaccounting.go index 9af55fbcc..fc0fc14ed 100644 --- a/satellite/satellitedb/projectaccounting.go +++ b/satellite/satellitedb/projectaccounting.go @@ -656,3 +656,20 @@ func (db *ProjectAccounting) getBuckets(ctx context.Context, projectID uuid.UUID func timeTruncateDown(t time.Time) time.Time { return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, t.Location()) } + +// GetProjectLimits returns current project limit for both storage and bandwidth. +func (db *ProjectAccounting) GetProjectLimits(ctx context.Context, projectID uuid.UUID) (_ accounting.ProjectLimits, err error) { + defer mon.Task()(&ctx)(&err) + + row, err := db.db.Get_Project_BandwidthLimit_Project_UsageLimit_By_Id(ctx, + dbx.Project_Id(projectID[:]), + ) + if err != nil { + return accounting.ProjectLimits{}, err + } + + return accounting.ProjectLimits{ + Usage: &row.UsageLimit, + Bandwidth: &row.BandwidthLimit, + }, nil +} diff --git a/scripts/testdata/satellite-config.yaml.lock b/scripts/testdata/satellite-config.yaml.lock index 432c7d875..701b2919d 100644 --- a/scripts/testdata/satellite-config.yaml.lock +++ b/scripts/testdata/satellite-config.yaml.lock @@ -607,6 +607,12 @@ identity.key-path: /root/.local/share/storj/identity/satellite/identity.key # number of months of project bandwidth rollups to retain, not including the current month # project-bw-cleanup.retain-months: 2 +# number of projects to cache. +# project-limit.cache-capacity: 10000 + +# how long to cache the project limits. +# project-limit.cache-expiration: 10m0s + # the URL for referral manager # referrals.referral-manager-url: ""