Add GC mechanism to secret manager

When Generate is called and the size is larger than cap, GC will be
triggered.

Signed-off-by: Daniel Jiang <jiangd@vmware.com>
This commit is contained in:
Daniel Jiang 2020-07-20 14:23:49 +08:00
parent 840aa86dfa
commit 14203169bf
2 changed files with 112 additions and 9 deletions

View File

@ -2,12 +2,18 @@ package secret
import ( import (
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/lib/log"
) )
const defaultExpiration = 15 * time.Second const (
defaultExpiration = 15 * time.Second
defaultGCInterval = 10 * time.Second
defaultCap uint64 = 1024 * 1024
)
type targetRepository struct { type targetRepository struct {
name string name string
@ -25,14 +31,40 @@ type Manager interface {
Verify(secret, repository string) bool Verify(secret, repository string) bool
} }
type flag struct {
v uint32
}
func (f *flag) grab() bool {
return atomic.CompareAndSwapUint32(&f.v, 0, 1)
}
func (f *flag) release() {
atomic.StoreUint32(&f.v, 0)
}
type mgr struct { type mgr struct {
gcFlag *flag
gcScheduleFlag *flag
// the minimal interval for gc
gcInterval time.Duration
lastGC time.Time
// the size of the map, it must be read and write via atomic
size uint64
// the capacity of the map, if the size is above the cap the gc will be triggered
cap uint64
m *sync.Map m *sync.Map
lock sync.Mutex
exp time.Duration exp time.Duration
} }
func (man *mgr) Generate(rn string) string { func (man *mgr) Generate(rn string) string {
if atomic.LoadUint64(&man.size) > man.cap {
man.gc()
}
s := utils.GenerateRandomStringWithLen(8) s := utils.GenerateRandomStringWithLen(8)
man.m.Store(s, targetRepository{name: rn, expiresAt: time.Now().Add(man.exp)}) man.m.Store(s, targetRepository{name: rn, expiresAt: time.Now().Add(man.exp)})
atomic.AddUint64(&man.size, 1)
return s return s
} }
@ -43,12 +75,59 @@ func (man *mgr) Verify(sec, rn string) bool {
} }
p, ok := v.(targetRepository) p, ok := v.(targetRepository)
if ok && p.name == rn { if ok && p.name == rn {
defer man.m.Delete(sec) defer man.delete(sec)
return p.expiresAt.After(time.Now()) return p.expiresAt.After(time.Now())
} }
return false return false
} }
func (man *mgr) delete(sec string) {
if _, ok := man.m.Load(sec); ok {
man.lock.Lock()
defer man.lock.Unlock()
if _, ok := man.m.Load(sec); ok {
man.m.Delete(sec)
atomic.AddUint64(&man.size, ^uint64(0))
}
}
}
// gc removes the expired entries so it's possible that after running gc the size is still larger than cap
// If that happens it will try to start a go routine to run another gc
func (man *mgr) gc() {
if !man.gcFlag.grab() {
log.Debugf("There is GC in progress, skip")
return
}
defer func() {
if atomic.LoadUint64(&man.size) > man.cap && man.gcScheduleFlag.grab() {
log.Debugf("Size is still larger than cap, schedule a gc in next cycle")
go func() {
time.Sleep(man.gcInterval)
man.gcScheduleFlag.release()
man.gc()
}()
}
man.gcFlag.release()
}()
if time.Now().Before(man.lastGC.Add(man.gcInterval)) {
log.Debugf("Skip too frequent GC, last one: %v, ", man.lastGC)
return
}
log.Debugf("Running GC on secret map...")
man.m.Range(func(k, v interface{}) bool {
repoV, ok := v.(targetRepository)
if ok && repoV.expiresAt.Before(time.Now()) {
log.Debugf("Removed expire secret: %s, repo: %s", k, repoV.name)
man.delete(k.(string))
}
return true
})
man.lastGC = time.Now()
log.Debugf("GC on secret map finished.")
}
var ( var (
defaultManager Manager defaultManager Manager
once sync.Once once sync.Once
@ -57,14 +136,18 @@ var (
// GetManager returns the default manager which is a singleton in the package // GetManager returns the default manager which is a singleton in the package
func GetManager() Manager { func GetManager() Manager {
once.Do(func() { once.Do(func() {
defaultManager = createManager(defaultExpiration) defaultManager = createManager(defaultExpiration, defaultCap, defaultGCInterval)
}) })
return defaultManager return defaultManager
} }
func createManager(d time.Duration) Manager { func createManager(d time.Duration, c uint64, interval time.Duration) Manager {
return &mgr{ return &mgr{
m: &sync.Map{}, m: &sync.Map{},
exp: d, exp: d,
cap: c,
gcInterval: interval,
gcFlag: &flag{},
gcScheduleFlag: &flag{},
} }
} }

View File

@ -1,6 +1,8 @@
package secret package secret
import ( import (
"fmt"
"sync/atomic"
"testing" "testing"
"time" "time"
@ -24,10 +26,28 @@ func TestManger(t *testing.T) {
} }
func TestExpiration(t *testing.T) { func TestExpiration(t *testing.T) {
manager := createManager(1 * time.Second) manager := createManager(1*time.Second, defaultCap, defaultGCInterval)
rn1 := "project1/golang" rn1 := "project1/golang"
s := manager.Generate(rn1) s := manager.Generate(rn1)
// Sleep till the secret expires // Sleep till the secret expires
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
assert.False(t, manager.Verify(s, rn1)) assert.False(t, manager.Verify(s, rn1))
} }
func TestGC(t *testing.T) {
manager := createManager(1*time.Second, 10, 1*time.Second).(*mgr)
for i := 0; i < 10; i++ {
rn := fmt.Sprintf("project%d/golang", i)
manager.Generate(rn)
}
time.Sleep(2 * time.Second)
assert.Equal(t, uint64(10), manager.size)
for i := 0; i < 1000; i++ {
rn := fmt.Sprintf("project%d/redis", i)
manager.Generate(rn)
}
assert.Equal(t, uint64(1000), atomic.LoadUint64(&manager.size))
time.Sleep(4 * time.Second)
assert.Equal(t, uint64(0), atomic.LoadUint64(&manager.size))
}