2021-04-18 15:41:13 -04:00
|
|
|
// Copyright (c) 2015-2021 MinIO, Inc.
|
|
|
|
//
|
|
|
|
// This file is part of MinIO Object Storage stack
|
|
|
|
//
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// This program is distributed in the hope that it will be useful
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU Affero General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
2020-04-30 18:55:54 -04:00
|
|
|
|
|
|
|
package cmd
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"time"
|
|
|
|
|
2021-05-06 11:52:02 -04:00
|
|
|
"github.com/minio/madmin-go"
|
2021-06-01 17:59:40 -04:00
|
|
|
"github.com/minio/minio/internal/event"
|
|
|
|
"github.com/minio/minio/internal/logger"
|
2020-04-30 18:55:54 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
// BucketQuotaSys - map of bucket and quota configuration.
|
2020-05-27 09:45:43 -04:00
|
|
|
type BucketQuotaSys struct {
|
|
|
|
bucketStorageCache timedValue
|
|
|
|
}
|
2020-04-30 18:55:54 -04:00
|
|
|
|
|
|
|
// Get - Get quota configuration.
|
2020-05-20 13:18:15 -04:00
|
|
|
func (sys *BucketQuotaSys) Get(bucketName string) (*madmin.BucketQuota, error) {
|
2020-05-19 16:53:54 -04:00
|
|
|
if globalIsGateway {
|
|
|
|
objAPI := newObjectLayerFn()
|
|
|
|
if objAPI == nil {
|
2020-05-20 13:18:15 -04:00
|
|
|
return nil, errServerNotInitialized
|
2020-05-19 16:53:54 -04:00
|
|
|
}
|
2020-05-20 13:18:15 -04:00
|
|
|
return &madmin.BucketQuota{}, nil
|
2020-05-19 16:53:54 -04:00
|
|
|
}
|
2020-04-30 18:55:54 -04:00
|
|
|
|
2020-05-20 13:18:15 -04:00
|
|
|
return globalBucketMetadataSys.GetQuotaConfig(bucketName)
|
2020-04-30 18:55:54 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewBucketQuotaSys returns initialized BucketQuotaSys
|
|
|
|
func NewBucketQuotaSys() *BucketQuotaSys {
|
2020-05-20 13:18:15 -04:00
|
|
|
return &BucketQuotaSys{}
|
2020-04-30 18:55:54 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// parseBucketQuota parses BucketQuota from json
|
2020-05-20 13:18:15 -04:00
|
|
|
func parseBucketQuota(bucket string, data []byte) (quotaCfg *madmin.BucketQuota, err error) {
|
|
|
|
quotaCfg = &madmin.BucketQuota{}
|
|
|
|
if err = json.Unmarshal(data, quotaCfg); err != nil {
|
2020-05-19 16:53:54 -04:00
|
|
|
return quotaCfg, err
|
2020-04-30 18:55:54 -04:00
|
|
|
}
|
2020-05-20 13:18:15 -04:00
|
|
|
if !quotaCfg.IsValid() {
|
|
|
|
return quotaCfg, fmt.Errorf("Invalid quota config %#v", quotaCfg)
|
2020-04-30 18:55:54 -04:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-05-27 09:45:43 -04:00
|
|
|
func (sys *BucketQuotaSys) check(ctx context.Context, bucket string, size int64) error {
|
2020-10-09 12:59:52 -04:00
|
|
|
objAPI := newObjectLayerFn()
|
2020-05-27 09:45:43 -04:00
|
|
|
if objAPI == nil {
|
|
|
|
return errServerNotInitialized
|
|
|
|
}
|
|
|
|
|
|
|
|
sys.bucketStorageCache.Once.Do(func() {
|
2020-07-24 15:24:21 -04:00
|
|
|
sys.bucketStorageCache.TTL = 1 * time.Second
|
2020-05-27 09:45:43 -04:00
|
|
|
sys.bucketStorageCache.Update = func() (interface{}, error) {
|
2020-09-23 11:30:31 -04:00
|
|
|
ctx, done := context.WithTimeout(context.Background(), 5*time.Second)
|
|
|
|
defer done()
|
2020-05-27 09:45:43 -04:00
|
|
|
return loadDataUsageFromBackend(ctx, objAPI)
|
2020-04-30 18:55:54 -04:00
|
|
|
}
|
2020-05-27 09:45:43 -04:00
|
|
|
})
|
|
|
|
|
2020-07-24 15:24:21 -04:00
|
|
|
q, err := sys.Get(bucket)
|
2020-05-27 09:45:43 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-07-24 15:24:21 -04:00
|
|
|
if q != nil && q.Type == madmin.HardQuota && q.Quota > 0 {
|
|
|
|
v, err := sys.bucketStorageCache.Get()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-05-27 09:45:43 -04:00
|
|
|
|
2021-10-11 14:03:54 -04:00
|
|
|
dui, ok := v.(DataUsageInfo)
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("internal error: Unexpected DUI data type: %T", v)
|
|
|
|
}
|
2020-05-27 09:45:43 -04:00
|
|
|
|
2020-07-24 15:24:21 -04:00
|
|
|
bui, ok := dui.BucketsUsage[bucket]
|
|
|
|
if !ok {
|
|
|
|
// bucket not found, cannot enforce quota
|
|
|
|
// call will fail anyways later.
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if (bui.Size + uint64(size)) >= q.Quota {
|
|
|
|
return BucketQuotaExceeded{Bucket: bucket}
|
|
|
|
}
|
2020-04-30 18:55:54 -04:00
|
|
|
}
|
2020-05-27 09:45:43 -04:00
|
|
|
|
2020-04-30 18:55:54 -04:00
|
|
|
return nil
|
|
|
|
}
|
2020-05-27 09:45:43 -04:00
|
|
|
|
2020-04-30 18:55:54 -04:00
|
|
|
func enforceBucketQuota(ctx context.Context, bucket string, size int64) error {
|
|
|
|
if size < 0 {
|
|
|
|
return nil
|
|
|
|
}
|
2020-05-19 16:53:54 -04:00
|
|
|
|
2020-05-27 09:45:43 -04:00
|
|
|
return globalBucketQuotaSys.check(ctx, bucket, size)
|
2020-04-30 18:55:54 -04:00
|
|
|
}
|
|
|
|
|
2020-07-14 21:59:05 -04:00
|
|
|
// enforceFIFOQuota deletes objects in FIFO order until sufficient objects
|
|
|
|
// have been deleted so as to bring bucket usage within quota.
|
2021-09-18 16:31:35 -04:00
|
|
|
func enforceFIFOQuotaBucket(ctx context.Context, objectAPI ObjectLayer, bucket string, bui BucketUsageInfo) {
|
2020-07-14 21:59:05 -04:00
|
|
|
// Check if the current bucket has quota restrictions, if not skip it
|
|
|
|
cfg, err := globalBucketQuotaSys.Get(bucket)
|
|
|
|
if err != nil {
|
|
|
|
return
|
2020-04-30 18:55:54 -04:00
|
|
|
}
|
|
|
|
|
2020-07-14 21:59:05 -04:00
|
|
|
if cfg.Type != madmin.FIFOQuota {
|
|
|
|
return
|
|
|
|
}
|
2020-04-30 18:55:54 -04:00
|
|
|
|
2020-07-14 21:59:05 -04:00
|
|
|
var toFree uint64
|
|
|
|
if bui.Size > cfg.Quota && cfg.Quota > 0 {
|
|
|
|
toFree = bui.Size - cfg.Quota
|
2020-04-30 18:55:54 -04:00
|
|
|
}
|
|
|
|
|
2020-07-14 21:59:05 -04:00
|
|
|
if toFree <= 0 {
|
2020-06-12 23:04:01 -04:00
|
|
|
return
|
2020-04-30 18:55:54 -04:00
|
|
|
}
|
2020-05-27 09:45:43 -04:00
|
|
|
|
2020-07-14 21:59:05 -04:00
|
|
|
// Allocate new results channel to receive ObjectInfo.
|
|
|
|
objInfoCh := make(chan ObjectInfo)
|
|
|
|
|
|
|
|
versioned := globalBucketVersioningSys.Enabled(bucket)
|
|
|
|
|
|
|
|
// Walk through all objects
|
|
|
|
if err := objectAPI.Walk(ctx, bucket, "", objInfoCh, ObjectOptions{WalkVersions: versioned}); err != nil {
|
2020-06-12 23:04:01 -04:00
|
|
|
logger.LogIf(ctx, err)
|
|
|
|
return
|
2020-05-20 13:18:15 -04:00
|
|
|
}
|
2020-05-27 09:45:43 -04:00
|
|
|
|
2020-07-14 21:59:05 -04:00
|
|
|
// reuse the fileScorer used by disk cache to score entries by
|
|
|
|
// ModTime to find the oldest objects in bucket to delete. In
|
|
|
|
// the context of bucket quota enforcement - number of hits are
|
|
|
|
// irrelevant.
|
2022-01-05 20:05:28 -05:00
|
|
|
scorer, err := newFileScorer(toFree, UTCNow().Unix(), 1)
|
2020-05-27 09:45:43 -04:00
|
|
|
if err != nil {
|
2020-06-12 23:04:01 -04:00
|
|
|
logger.LogIf(ctx, err)
|
|
|
|
return
|
2020-05-27 09:45:43 -04:00
|
|
|
}
|
|
|
|
|
2020-07-14 21:59:05 -04:00
|
|
|
rcfg, _ := globalBucketObjectLockSys.Get(bucket)
|
|
|
|
for obj := range objInfoCh {
|
|
|
|
// skip objects currently under retention
|
|
|
|
if rcfg.LockEnabled && enforceRetentionForDeletion(ctx, obj) {
|
2020-04-30 18:55:54 -04:00
|
|
|
continue
|
|
|
|
}
|
2020-07-14 21:59:05 -04:00
|
|
|
scorer.addFileWithObjInfo(obj, 1)
|
|
|
|
}
|
2020-05-19 16:53:54 -04:00
|
|
|
|
2020-07-14 21:59:05 -04:00
|
|
|
// If we saw less than quota we are good.
|
|
|
|
if scorer.seenBytes <= cfg.Quota {
|
|
|
|
return
|
|
|
|
}
|
2022-01-05 20:05:28 -05:00
|
|
|
|
2020-07-14 21:59:05 -04:00
|
|
|
// Calculate how much we want to delete now.
|
|
|
|
toFreeNow := scorer.seenBytes - cfg.Quota
|
|
|
|
// We were less over quota than we thought. Adjust so we delete less.
|
|
|
|
// If we are more over, leave it for the next run to pick up.
|
|
|
|
if toFreeNow < toFree {
|
|
|
|
if !scorer.adjustSaveBytes(int64(toFreeNow) - int64(toFree)) {
|
|
|
|
// We got below or at quota.
|
|
|
|
return
|
2020-04-30 18:55:54 -04:00
|
|
|
}
|
2020-07-14 21:59:05 -04:00
|
|
|
}
|
2020-05-27 09:45:43 -04:00
|
|
|
|
2020-07-14 21:59:05 -04:00
|
|
|
var objects []ObjectToDelete
|
|
|
|
numKeys := len(scorer.fileObjInfos())
|
|
|
|
for i, obj := range scorer.fileObjInfos() {
|
|
|
|
objects = append(objects, ObjectToDelete{
|
2022-01-03 04:28:52 -05:00
|
|
|
ObjectV: ObjectV{
|
|
|
|
ObjectName: obj.Name,
|
|
|
|
VersionID: obj.VersionID,
|
|
|
|
},
|
2020-07-14 21:59:05 -04:00
|
|
|
})
|
|
|
|
if len(objects) < maxDeleteList && (i < numKeys-1) {
|
|
|
|
// skip deletion until maxDeleteList or end of slice
|
2020-06-12 23:04:01 -04:00
|
|
|
continue
|
2020-04-30 18:55:54 -04:00
|
|
|
}
|
|
|
|
|
2020-07-14 21:59:05 -04:00
|
|
|
if len(objects) == 0 {
|
|
|
|
break
|
2020-04-30 18:55:54 -04:00
|
|
|
}
|
2020-06-12 23:04:01 -04:00
|
|
|
|
2020-07-14 21:59:05 -04:00
|
|
|
// Deletes a list of objects.
|
|
|
|
_, deleteErrs := objectAPI.DeleteObjects(ctx, bucket, objects, ObjectOptions{
|
|
|
|
Versioned: versioned,
|
|
|
|
})
|
2022-01-05 20:05:28 -05:00
|
|
|
for j := range deleteErrs {
|
|
|
|
if deleteErrs[j] != nil {
|
|
|
|
logger.LogIf(ctx, deleteErrs[j])
|
2020-04-30 18:55:54 -04:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2020-07-14 21:59:05 -04:00
|
|
|
// Notify object deleted event.
|
|
|
|
sendEvent(eventArgs{
|
|
|
|
EventName: event.ObjectRemovedDelete,
|
|
|
|
BucketName: bucket,
|
|
|
|
Object: obj,
|
|
|
|
Host: "Internal: [FIFO-QUOTA-EXPIRY]",
|
2020-06-12 23:04:01 -04:00
|
|
|
})
|
2020-04-30 18:55:54 -04:00
|
|
|
}
|
2020-07-14 21:59:05 -04:00
|
|
|
objects = nil
|
2020-04-30 18:55:54 -04:00
|
|
|
}
|
|
|
|
}
|