1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819package limiters2021import (22 "context"23 "sync"24 "time"25)2627// BucketSet combines a group of Ls into a single key-indexed structure.28// Basically, each unique key gets its own counter. The main use case for29// BucketSet is to apply per-resource rate limiting.30//31// Amount of buckets is limited to a certain value. When the size of internal32// map is around or equal to that value, next Take call will attempt to remove33// any stale buckets from the group. If it is not possible to do so (all34// buckets are in active use), Take will return false. Alternatively, in some35// rare cases, some other (undefined) waiting Take can return false.36//37// A BucksetSet without a New function assigned is no-op: Take and TakeContext38// always succeed and Release does nothing.39type BucketSet struct {40 // New function is used to construct underlying L instances.41 //42 // It is safe to change it only when BucketSet is not used by any43 // goroutine.44 New func() L4546 // Time after which bucket is considered stale and can be removed from the47 // set. For safe use with Rate limiter, it should be at least as twice as48 // big as Rate refill interval.49 ReapInterval time.Duration5051 MaxBuckets int5253 mLck sync.Mutex54 m map[string]*struct {55 r L56 lastUse time.Time57 }58}5960func NewBucketSet(new_ func() L, reapInterval time.Duration, maxBuckets int) *BucketSet {61 return &BucketSet{62 New: new_,63 ReapInterval: reapInterval,64 MaxBuckets: maxBuckets,65 m: map[string]*struct {66 r L67 lastUse time.Time68 }{},69 }70}7172func (r *BucketSet) Close() {73 r.mLck.Lock()74 defer r.mLck.Unlock()7576 for _, v := range r.m {77 v.r.Close()78 }79}8081func (r *BucketSet) take(key string) L {82 r.mLck.Lock()83 defer r.mLck.Unlock()8485 if len(r.m) > r.MaxBuckets {86 now := time.Now()87 // Attempt to get rid of stale buckets.88 for k, v := range r.m {89 if v.lastUse.Sub(now) > r.ReapInterval {90 // Drop the bucket, if there happen to be any waiting Take for it.91 // It will return 'false', but this is fine for us since this92 // whole 'reaping' process will run only when we are under a93 // high load and dropping random requests in this case is a94 // more or less reasonable thing to do.95 v.r.Close()96 delete(r.m, k)97 }98 }99100 // Still full? E.g. all buckets are in use.101 if len(r.m) > r.MaxBuckets {102 return nil103 }104 }105106 bucket, ok := r.m[key]107 if !ok {108 r.m[key] = &struct {109 r L110 lastUse time.Time111 }{112 r: r.New(),113 lastUse: time.Now(),114 }115 bucket = r.m[key]116 }117 r.m[key].lastUse = time.Now()118119 return bucket.r120}121122func (r *BucketSet) Take(key string) bool {123 if r.New == nil {124 return true125 }126127 bucket := r.take(key)128 return bucket.Take()129}130131func (r *BucketSet) Release(key string) {132 if r.New == nil {133 return134 }135136 r.mLck.Lock()137 defer r.mLck.Unlock()138139 bucket, ok := r.m[key]140 if !ok {141 return142 }143 bucket.r.Release()144}145146func (r *BucketSet) TakeContext(ctx context.Context, key string) error {147 if r.New == nil {148 return nil149 }150151 bucket := r.take(key)152 return bucket.TakeContext(ctx)153}