From 07d25c4dacc02aabe0aab083e22dde60500f6c48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Tue, 18 Oct 2022 12:28:45 +0200 Subject: [PATCH 1/6] improve memory storage code and performance --- internal/storage/memory/config.go | 33 ++++ internal/storage/memory/memory.go | 40 +++-- internal/storage/memory/memory_test.go | 204 +++++++++++++++++++++++++ utils/time.go | 28 ++++ 4 files changed, 294 insertions(+), 11 deletions(-) create mode 100644 internal/storage/memory/config.go create mode 100644 internal/storage/memory/memory_test.go create mode 100644 utils/time.go diff --git a/internal/storage/memory/config.go b/internal/storage/memory/config.go new file mode 100644 index 0000000000..07d13edb5b --- /dev/null +++ b/internal/storage/memory/config.go @@ -0,0 +1,33 @@ +package memory + +import "time" + +// Config defines the config for storage. +type Config struct { + // Time before deleting expired keys + // + // Default is 10 * time.Second + GCInterval time.Duration +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + GCInterval: 10 * time.Second, +} + +// configDefault is a helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if int(cfg.GCInterval.Seconds()) <= 0 { + cfg.GCInterval = ConfigDefault.GCInterval + } + return cfg +} diff --git a/internal/storage/memory/memory.go b/internal/storage/memory/memory.go index 048fb3a325..51bd8e3816 100644 --- a/internal/storage/memory/memory.go +++ b/internal/storage/memory/memory.go @@ -2,7 +2,10 @@ package memory import ( "sync" + "sync/atomic" "time" + + "github.com/gofiber/fiber/v2/utils" ) // Storage interface that is implemented by storage providers @@ -14,21 +17,25 @@ type Storage struct { } type entry struct { + data []byte // max value is 4294967295 -> Sun Feb 07 2106 06:28:15 GMT+0000 expiry uint32 - data []byte } // New creates a new memory storage -func New() *Storage { +func New(config ...Config) *Storage { + // Set default config + cfg := configDefault(config...) + // Create storage store := &Storage{ db: make(map[string]entry), - gcInterval: 10 * time.Second, + gcInterval: cfg.GCInterval, done: make(chan struct{}), } // Start garbage collector + utils.StartTimeStampUpdater() go store.gc() return store @@ -42,7 +49,7 @@ func (s *Storage) Get(key string) ([]byte, error) { s.mux.RLock() v, ok := s.db[key] s.mux.RUnlock() - if !ok || v.expiry != 0 && v.expiry <= uint32(time.Now().Unix()) { + if !ok || v.expiry != 0 && v.expiry <= atomic.LoadUint32(&utils.Timestamp) { return nil, nil } @@ -58,11 +65,11 @@ func (s *Storage) Set(key string, val []byte, exp time.Duration) error { var expire uint32 if exp != 0 { - expire = uint32(time.Now().Add(exp).Unix()) + expire = uint32(exp.Seconds()) + atomic.LoadUint32(&utils.Timestamp) } s.mux.Lock() - s.db[key] = entry{expire, val} + s.db[key] = entry{val, expire} s.mux.Unlock() return nil } @@ -96,20 +103,31 @@ func (s *Storage) Close() error { func (s *Storage) gc() { ticker := time.NewTicker(s.gcInterval) defer ticker.Stop() + var expired []string for { select { case <-s.done: return - case t := <-ticker.C: - now := uint32(t.Unix()) - s.mux.Lock() + case <-ticker.C: + expired = expired[:0] + s.mux.RLock() for id, v := range s.db { - if v.expiry != 0 && v.expiry < now { - delete(s.db, id) + if v.expiry != 0 && v.expiry < atomic.LoadUint32(&utils.Timestamp) { + expired = append(expired, id) } } + s.mux.RUnlock() + s.mux.Lock() + for i := range expired { + delete(s.db, expired[i]) + } s.mux.Unlock() } } } + +// Return database client +func (s *Storage) Conn() map[string]entry { + return s.db +} diff --git a/internal/storage/memory/memory_test.go b/internal/storage/memory/memory_test.go new file mode 100644 index 0000000000..c92920807e --- /dev/null +++ b/internal/storage/memory/memory_test.go @@ -0,0 +1,204 @@ +package memory + +import ( + "testing" + "time" + + "github.com/gofiber/fiber/v2/utils" +) + +var testStore = New() + +func Test_Memory_Set(t *testing.T) { + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) +} + +func Test_Memory_Set_Override(t *testing.T) { + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) +} + +func Test_Memory_Get(t *testing.T) { + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) + + result, err := testStore.Get(key) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, val, result) +} + +func Test_Memory_Set_Expiration(t *testing.T) { + var ( + key = "john" + val = []byte("doe") + exp = 1 * time.Second + ) + + err := testStore.Set(key, val, exp) + utils.AssertEqual(t, nil, err) + + time.Sleep(1100 * time.Millisecond) +} + +func Test_Memory_Get_Expired(t *testing.T) { + var ( + key = "john" + ) + + result, err := testStore.Get(key) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) +} + +func Test_Memory_Get_NotExist(t *testing.T) { + + result, err := testStore.Get("notexist") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) +} + +func Test_Memory_Delete(t *testing.T) { + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Delete(key) + utils.AssertEqual(t, nil, err) + + result, err := testStore.Get(key) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) +} + +func Test_Memory_Reset(t *testing.T) { + var ( + val = []byte("doe") + ) + + err := testStore.Set("john1", val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Set("john2", val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Reset() + utils.AssertEqual(t, nil, err) + + result, err := testStore.Get("john1") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) + + result, err = testStore.Get("john2") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) +} + +func Test_Memory_Close(t *testing.T) { + utils.AssertEqual(t, nil, testStore.Close()) +} + +func Test_Memory_Conn(t *testing.T) { + utils.AssertEqual(t, true, testStore.Conn() != nil) +} + +// go test -run Test_Memory -v -race + +func Test_Memory(t *testing.T) { + var store = New() + var ( + key = "john" + val = []byte("doe") + exp = 1 * time.Second + ) + + store.Set(key, val, 0) + + result, error := store.Get(key) + utils.AssertEqual(t, val, result) + utils.AssertEqual(t, nil, error) + + result, error = store.Get("empty") + utils.AssertEqual(t, nil, result) + utils.AssertEqual(t, nil, error) + + store.Set(key, val, exp) + time.Sleep(1100 * time.Millisecond) + + result, error = store.Get(key) + utils.AssertEqual(t, nil, result) + utils.AssertEqual(t, nil, error) + + store.Set(key, val, 0) + result, error = store.Get(key) + utils.AssertEqual(t, val, result) + utils.AssertEqual(t, nil, error) + + store.Delete(key) + result, error = store.Get(key) + utils.AssertEqual(t, nil, result) + utils.AssertEqual(t, nil, error) + + store.Set("john", val, 0) + store.Set("doe", val, 0) + store.Reset() + + result, error = store.Get("john") + utils.AssertEqual(t, nil, result) + utils.AssertEqual(t, nil, error) + + result, error = store.Get("doe") + utils.AssertEqual(t, nil, result) + utils.AssertEqual(t, nil, error) + +} + +// go test -v -run=^$ -bench=Benchmark_Memory -benchmem -count=4 +func Benchmark_Memory(b *testing.B) { + keyLength := 1000 + keys := make([]string, keyLength) + for i := 0; i < keyLength; i++ { + keys[i] = utils.UUID() + } + value := []byte("joe") + + ttl := 2 * time.Second + b.Run("fiber_memory", func(b *testing.B) { + d := New() + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + for _, key := range keys { + d.Set(key, value, ttl) + } + for _, key := range keys { + _, _ = d.Get(key) + } + for _, key := range keys { + d.Delete(key) + } + } + }) +} diff --git a/utils/time.go b/utils/time.go new file mode 100644 index 0000000000..27b8aeb12f --- /dev/null +++ b/utils/time.go @@ -0,0 +1,28 @@ +package utils + +import ( + "sync" + "sync/atomic" + "time" +) + +var ( + timestampTimer sync.Once + Timestamp uint32 +) + +func StartTimeStampUpdater() { + timestampTimer.Do(func() { + go func(sleep time.Duration) { + ticker := time.NewTicker(sleep) + defer ticker.Stop() + for { + select { + case t := <-ticker.C: + // update timestamp + atomic.StoreUint32(&Timestamp, uint32(t.Unix())) + } + } + }(1 * time.Second) // duration + }) +} From 141908d31edc72c56743db62b9a8801026bb5abc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Tue, 18 Oct 2022 13:11:04 +0200 Subject: [PATCH 2/6] improve memory storage code and performance --- internal/memory/memory.go | 50 ++++++++++++------------- internal/memory/memory_test.go | 2 +- internal/storage/memory/memory.go | 9 +++-- internal/storage/memory/memory_test.go | 51 -------------------------- middleware/limiter/limiter_fixed.go | 13 ++----- middleware/limiter/limiter_sliding.go | 12 ++---- utils/time.go | 4 +- 7 files changed, 39 insertions(+), 102 deletions(-) diff --git a/internal/memory/memory.go b/internal/memory/memory.go index dd8111a361..8d57308df7 100644 --- a/internal/memory/memory.go +++ b/internal/memory/memory.go @@ -1,15 +1,17 @@ +// Package memory Is a slight copy of the memory storage, but far from the storage interface it can not only work with bytes +// but directly store any kind of data without having to encode it each time, which gives a huge speed advantage package memory import ( "sync" - "sync/atomic" "time" + + "github.com/gofiber/fiber/v2/utils" ) type Storage struct { sync.RWMutex data map[string]item // data - ts uint32 // timestamp } type item struct { @@ -21,10 +23,9 @@ type item struct { func New() *Storage { store := &Storage{ data: make(map[string]item), - ts: uint32(time.Now().Unix()), } + utils.StartTimeStampUpdater() go store.gc(1 * time.Second) - go store.updater(1 * time.Second) return store } @@ -33,7 +34,7 @@ func (s *Storage) Get(key string) interface{} { s.RLock() v, ok := s.data[key] s.RUnlock() - if !ok || v.e != 0 && v.e <= atomic.LoadUint32(&s.ts) { + if !ok || v.e != 0 && v.e <= utils.Timestamp.Load() { return nil } return v.v @@ -43,7 +44,7 @@ func (s *Storage) Get(key string) interface{} { func (s *Storage) Set(key string, val interface{}, ttl time.Duration) { var exp uint32 if ttl > 0 { - exp = uint32(ttl.Seconds()) + atomic.LoadUint32(&s.ts) + exp = uint32(ttl.Seconds()) + utils.Timestamp.Load() } s.Lock() s.data[key] = item{exp, val} @@ -64,28 +65,27 @@ func (s *Storage) Reset() { s.Unlock() } -func (s *Storage) updater(sleep time.Duration) { - for { - time.Sleep(sleep) - atomic.StoreUint32(&s.ts, uint32(time.Now().Unix())) - } -} func (s *Storage) gc(sleep time.Duration) { - expired := []string{} + ticker := time.NewTicker(sleep) + defer ticker.Stop() + var expired []string + for { - time.Sleep(sleep) - expired = expired[:0] - s.RLock() - for key, v := range s.data { - if v.e != 0 && v.e <= atomic.LoadUint32(&s.ts) { - expired = append(expired, key) + select { + case <-ticker.C: + expired = expired[:0] + s.RLock() + for key, v := range s.data { + if v.e != 0 && v.e <= utils.Timestamp.Load() { + expired = append(expired, key) + } } + s.RUnlock() + s.Lock() + for i := range expired { + delete(s.data, expired[i]) + } + s.Unlock() } - s.RUnlock() - s.Lock() - for i := range expired { - delete(s.data, expired[i]) - } - s.Unlock() } } diff --git a/internal/memory/memory_test.go b/internal/memory/memory_test.go index a592bb7cba..12bcf3884c 100644 --- a/internal/memory/memory_test.go +++ b/internal/memory/memory_test.go @@ -58,7 +58,7 @@ func Benchmark_Memory(b *testing.B) { for i := 0; i < keyLength; i++ { keys[i] = utils.UUID() } - value := []string{"some", "random", "value"} + value := []byte("joe") ttl := 2 * time.Second b.Run("fiber_memory", func(b *testing.B) { diff --git a/internal/storage/memory/memory.go b/internal/storage/memory/memory.go index 51bd8e3816..46e00d332d 100644 --- a/internal/storage/memory/memory.go +++ b/internal/storage/memory/memory.go @@ -1,8 +1,9 @@ +// Package memory Is a copy of the storage memory from the external storage packet as a purpose to test the behavior +// in the unittests when using a storages from these packets package memory import ( "sync" - "sync/atomic" "time" "github.com/gofiber/fiber/v2/utils" @@ -49,7 +50,7 @@ func (s *Storage) Get(key string) ([]byte, error) { s.mux.RLock() v, ok := s.db[key] s.mux.RUnlock() - if !ok || v.expiry != 0 && v.expiry <= atomic.LoadUint32(&utils.Timestamp) { + if !ok || v.expiry != 0 && v.expiry <= utils.Timestamp.Load() { return nil, nil } @@ -65,7 +66,7 @@ func (s *Storage) Set(key string, val []byte, exp time.Duration) error { var expire uint32 if exp != 0 { - expire = uint32(exp.Seconds()) + atomic.LoadUint32(&utils.Timestamp) + expire = uint32(exp.Seconds()) + utils.Timestamp.Load() } s.mux.Lock() @@ -113,7 +114,7 @@ func (s *Storage) gc() { expired = expired[:0] s.mux.RLock() for id, v := range s.db { - if v.expiry != 0 && v.expiry < atomic.LoadUint32(&utils.Timestamp) { + if v.expiry != 0 && v.expiry < utils.Timestamp.Load() { expired = append(expired, id) } } diff --git a/internal/storage/memory/memory_test.go b/internal/storage/memory/memory_test.go index c92920807e..20b398a761 100644 --- a/internal/storage/memory/memory_test.go +++ b/internal/storage/memory/memory_test.go @@ -124,57 +124,6 @@ func Test_Memory_Conn(t *testing.T) { utils.AssertEqual(t, true, testStore.Conn() != nil) } -// go test -run Test_Memory -v -race - -func Test_Memory(t *testing.T) { - var store = New() - var ( - key = "john" - val = []byte("doe") - exp = 1 * time.Second - ) - - store.Set(key, val, 0) - - result, error := store.Get(key) - utils.AssertEqual(t, val, result) - utils.AssertEqual(t, nil, error) - - result, error = store.Get("empty") - utils.AssertEqual(t, nil, result) - utils.AssertEqual(t, nil, error) - - store.Set(key, val, exp) - time.Sleep(1100 * time.Millisecond) - - result, error = store.Get(key) - utils.AssertEqual(t, nil, result) - utils.AssertEqual(t, nil, error) - - store.Set(key, val, 0) - result, error = store.Get(key) - utils.AssertEqual(t, val, result) - utils.AssertEqual(t, nil, error) - - store.Delete(key) - result, error = store.Get(key) - utils.AssertEqual(t, nil, result) - utils.AssertEqual(t, nil, error) - - store.Set("john", val, 0) - store.Set("doe", val, 0) - store.Reset() - - result, error = store.Get("john") - utils.AssertEqual(t, nil, result) - utils.AssertEqual(t, nil, error) - - result, error = store.Get("doe") - utils.AssertEqual(t, nil, result) - utils.AssertEqual(t, nil, error) - -} - // go test -v -run=^$ -bench=Benchmark_Memory -benchmem -count=4 func Benchmark_Memory(b *testing.B) { keyLength := 1000 diff --git a/middleware/limiter/limiter_fixed.go b/middleware/limiter/limiter_fixed.go index ed1f23d523..69ec871a25 100644 --- a/middleware/limiter/limiter_fixed.go +++ b/middleware/limiter/limiter_fixed.go @@ -3,10 +3,9 @@ package limiter import ( "strconv" "sync" - "sync/atomic" - "time" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" ) type FixedWindow struct{} @@ -17,7 +16,6 @@ func (FixedWindow) New(cfg Config) fiber.Handler { // Limiter variables mux = &sync.RWMutex{} max = strconv.Itoa(cfg.Max) - timestamp = uint64(time.Now().Unix()) expiration = uint64(cfg.Expiration.Seconds()) ) @@ -25,12 +23,7 @@ func (FixedWindow) New(cfg Config) fiber.Handler { manager := newManager(cfg.Storage) // Update timestamp every second - go func() { - for { - atomic.StoreUint64(×tamp, uint64(time.Now().Unix())) - time.Sleep(1 * time.Second) - } - }() + utils.StartTimeStampUpdater() // Return new handler return func(c *fiber.Ctx) error { @@ -49,7 +42,7 @@ func (FixedWindow) New(cfg Config) fiber.Handler { e := manager.get(key) // Get timestamp - ts := atomic.LoadUint64(×tamp) + ts := uint64(utils.Timestamp.Load()) // Set expiration if entry does not exist if e.exp == 0 { diff --git a/middleware/limiter/limiter_sliding.go b/middleware/limiter/limiter_sliding.go index 9369998d29..3774206380 100644 --- a/middleware/limiter/limiter_sliding.go +++ b/middleware/limiter/limiter_sliding.go @@ -3,10 +3,10 @@ package limiter import ( "strconv" "sync" - "sync/atomic" "time" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" ) type SlidingWindow struct{} @@ -17,7 +17,6 @@ func (SlidingWindow) New(cfg Config) fiber.Handler { // Limiter variables mux = &sync.RWMutex{} max = strconv.Itoa(cfg.Max) - timestamp = uint64(time.Now().Unix()) expiration = uint64(cfg.Expiration.Seconds()) ) @@ -25,12 +24,7 @@ func (SlidingWindow) New(cfg Config) fiber.Handler { manager := newManager(cfg.Storage) // Update timestamp every second - go func() { - for { - atomic.StoreUint64(×tamp, uint64(time.Now().Unix())) - time.Sleep(1 * time.Second) - } - }() + utils.StartTimeStampUpdater() // Return new handler return func(c *fiber.Ctx) error { @@ -49,7 +43,7 @@ func (SlidingWindow) New(cfg Config) fiber.Handler { e := manager.get(key) // Get timestamp - ts := atomic.LoadUint64(×tamp) + ts := uint64(utils.Timestamp.Load()) // Set expiration if entry does not exist if e.exp == 0 { diff --git a/utils/time.go b/utils/time.go index 27b8aeb12f..250a19b7aa 100644 --- a/utils/time.go +++ b/utils/time.go @@ -8,7 +8,7 @@ import ( var ( timestampTimer sync.Once - Timestamp uint32 + Timestamp atomic.Uint32 ) func StartTimeStampUpdater() { @@ -20,7 +20,7 @@ func StartTimeStampUpdater() { select { case t := <-ticker.C: // update timestamp - atomic.StoreUint32(&Timestamp, uint32(t.Unix())) + Timestamp.Store(uint32(t.Unix())) } } }(1 * time.Second) // duration From 18376f5f566b3a93dada7331285074688c0182d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Tue, 18 Oct 2022 18:44:01 +0200 Subject: [PATCH 3/6] improve memory storage code and performance --- utils/time.go | 2 ++ utils/time_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 utils/time_test.go diff --git a/utils/time.go b/utils/time.go index 250a19b7aa..a774fd4f47 100644 --- a/utils/time.go +++ b/utils/time.go @@ -13,6 +13,8 @@ var ( func StartTimeStampUpdater() { timestampTimer.Do(func() { + // set initial value + Timestamp.Store(uint32(time.Now().Unix())) go func(sleep time.Duration) { ticker := time.NewTicker(sleep) defer ticker.Stop() diff --git a/utils/time_test.go b/utils/time_test.go new file mode 100644 index 0000000000..621b6c5179 --- /dev/null +++ b/utils/time_test.go @@ -0,0 +1,44 @@ +package utils + +import ( + "testing" + "time" +) + +func checkTimeStamp(t testing.TB, expectedCurrent, actualCurrent uint32) { + // test with some buffer in front and back of the expectedCurrent time -> because of the timing on the work machine + AssertEqual(t, true, actualCurrent >= expectedCurrent-1 || actualCurrent <= expectedCurrent+1) +} + +func Test_StartTimeStampUpdater(t *testing.T) { + t.Parallel() + + StartTimeStampUpdater() + + now := uint32(time.Now().Unix()) + checkTimeStamp(t, now, Timestamp.Load()) + // one second later + time.Sleep(1 * time.Second) + checkTimeStamp(t, now+1, Timestamp.Load()) + // two seconds later + time.Sleep(1 * time.Second) + checkTimeStamp(t, now+2, Timestamp.Load()) +} + +func Benchmark_CalculateTimestamp(b *testing.B) { + StartTimeStampUpdater() + + var res uint32 + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = Timestamp.Load() + } + checkTimeStamp(b, uint32(time.Now().Unix()), res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = uint32(time.Now().Unix()) + } + checkTimeStamp(b, uint32(time.Now().Unix()), res) + }) +} From 77b2bdd90abcda3b349893f70ea0596f640a0ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Wed, 19 Oct 2022 09:42:25 +0200 Subject: [PATCH 4/6] improve memory storage code and performance --- internal/memory/memory.go | 7 ++++--- internal/storage/memory/memory.go | 7 ++++--- middleware/limiter/limiter_fixed.go | 3 ++- middleware/limiter/limiter_sliding.go | 3 ++- utils/time.go | 18 ++++++++++-------- utils/time_test.go | 15 +++++++++------ 6 files changed, 31 insertions(+), 22 deletions(-) diff --git a/internal/memory/memory.go b/internal/memory/memory.go index 8d57308df7..0c37aa9c2e 100644 --- a/internal/memory/memory.go +++ b/internal/memory/memory.go @@ -4,6 +4,7 @@ package memory import ( "sync" + "sync/atomic" "time" "github.com/gofiber/fiber/v2/utils" @@ -34,7 +35,7 @@ func (s *Storage) Get(key string) interface{} { s.RLock() v, ok := s.data[key] s.RUnlock() - if !ok || v.e != 0 && v.e <= utils.Timestamp.Load() { + if !ok || v.e != 0 && v.e <= atomic.LoadUint32(&utils.Timestamp) { return nil } return v.v @@ -44,7 +45,7 @@ func (s *Storage) Get(key string) interface{} { func (s *Storage) Set(key string, val interface{}, ttl time.Duration) { var exp uint32 if ttl > 0 { - exp = uint32(ttl.Seconds()) + utils.Timestamp.Load() + exp = uint32(ttl.Seconds()) + atomic.LoadUint32(&utils.Timestamp) } s.Lock() s.data[key] = item{exp, val} @@ -76,7 +77,7 @@ func (s *Storage) gc(sleep time.Duration) { expired = expired[:0] s.RLock() for key, v := range s.data { - if v.e != 0 && v.e <= utils.Timestamp.Load() { + if v.e != 0 && v.e <= atomic.LoadUint32(&utils.Timestamp) { expired = append(expired, key) } } diff --git a/internal/storage/memory/memory.go b/internal/storage/memory/memory.go index 46e00d332d..ff43c30533 100644 --- a/internal/storage/memory/memory.go +++ b/internal/storage/memory/memory.go @@ -4,6 +4,7 @@ package memory import ( "sync" + "sync/atomic" "time" "github.com/gofiber/fiber/v2/utils" @@ -50,7 +51,7 @@ func (s *Storage) Get(key string) ([]byte, error) { s.mux.RLock() v, ok := s.db[key] s.mux.RUnlock() - if !ok || v.expiry != 0 && v.expiry <= utils.Timestamp.Load() { + if !ok || v.expiry != 0 && v.expiry <= atomic.LoadUint32(&utils.Timestamp) { return nil, nil } @@ -66,7 +67,7 @@ func (s *Storage) Set(key string, val []byte, exp time.Duration) error { var expire uint32 if exp != 0 { - expire = uint32(exp.Seconds()) + utils.Timestamp.Load() + expire = uint32(exp.Seconds()) + atomic.LoadUint32(&utils.Timestamp) } s.mux.Lock() @@ -114,7 +115,7 @@ func (s *Storage) gc() { expired = expired[:0] s.mux.RLock() for id, v := range s.db { - if v.expiry != 0 && v.expiry < utils.Timestamp.Load() { + if v.expiry != 0 && v.expiry < atomic.LoadUint32(&utils.Timestamp) { expired = append(expired, id) } } diff --git a/middleware/limiter/limiter_fixed.go b/middleware/limiter/limiter_fixed.go index 69ec871a25..b6b6d35939 100644 --- a/middleware/limiter/limiter_fixed.go +++ b/middleware/limiter/limiter_fixed.go @@ -3,6 +3,7 @@ package limiter import ( "strconv" "sync" + "sync/atomic" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/utils" @@ -42,7 +43,7 @@ func (FixedWindow) New(cfg Config) fiber.Handler { e := manager.get(key) // Get timestamp - ts := uint64(utils.Timestamp.Load()) + ts := uint64(atomic.LoadUint32(&utils.Timestamp)) // Set expiration if entry does not exist if e.exp == 0 { diff --git a/middleware/limiter/limiter_sliding.go b/middleware/limiter/limiter_sliding.go index 3774206380..7f49863d7a 100644 --- a/middleware/limiter/limiter_sliding.go +++ b/middleware/limiter/limiter_sliding.go @@ -3,6 +3,7 @@ package limiter import ( "strconv" "sync" + "sync/atomic" "time" "github.com/gofiber/fiber/v2" @@ -43,7 +44,7 @@ func (SlidingWindow) New(cfg Config) fiber.Handler { e := manager.get(key) // Get timestamp - ts := uint64(utils.Timestamp.Load()) + ts := uint64(atomic.LoadUint32(&utils.Timestamp)) // Set expiration if entry does not exist if e.exp == 0 { diff --git a/utils/time.go b/utils/time.go index a774fd4f47..8ea13c2262 100644 --- a/utils/time.go +++ b/utils/time.go @@ -8,22 +8,24 @@ import ( var ( timestampTimer sync.Once - Timestamp atomic.Uint32 + // Timestamp please start the timer function before you use this value + // please load the value with atomic `atomic.LoadUint32(&utils.Timestamp)` + Timestamp uint32 ) +// StartTimeStampUpdater starts a concurrent function which stores the timestamp to an atomic value per second, +// which is much better for performance than determining it at runtime each time func StartTimeStampUpdater() { timestampTimer.Do(func() { // set initial value - Timestamp.Store(uint32(time.Now().Unix())) + atomic.StoreUint32(&Timestamp, uint32(time.Now().Unix())) go func(sleep time.Duration) { ticker := time.NewTicker(sleep) defer ticker.Stop() - for { - select { - case t := <-ticker.C: - // update timestamp - Timestamp.Store(uint32(t.Unix())) - } + + for t := range ticker.C { + // update timestamp + atomic.StoreUint32(&Timestamp, uint32(t.Unix())) } }(1 * time.Second) // duration }) diff --git a/utils/time_test.go b/utils/time_test.go index 621b6c5179..2f74d55498 100644 --- a/utils/time_test.go +++ b/utils/time_test.go @@ -1,28 +1,31 @@ package utils import ( + "sync/atomic" "testing" "time" + + "github.com/stretchr/testify/require" ) func checkTimeStamp(t testing.TB, expectedCurrent, actualCurrent uint32) { // test with some buffer in front and back of the expectedCurrent time -> because of the timing on the work machine - AssertEqual(t, true, actualCurrent >= expectedCurrent-1 || actualCurrent <= expectedCurrent+1) + require.Equal(t, true, actualCurrent >= expectedCurrent-1 || actualCurrent <= expectedCurrent+1) } -func Test_StartTimeStampUpdater(t *testing.T) { +func Test_TimeStampUpdater(t *testing.T) { t.Parallel() StartTimeStampUpdater() now := uint32(time.Now().Unix()) - checkTimeStamp(t, now, Timestamp.Load()) + checkTimeStamp(t, now, atomic.LoadUint32(&Timestamp)) // one second later time.Sleep(1 * time.Second) - checkTimeStamp(t, now+1, Timestamp.Load()) + checkTimeStamp(t, now+1, atomic.LoadUint32(&Timestamp)) // two seconds later time.Sleep(1 * time.Second) - checkTimeStamp(t, now+2, Timestamp.Load()) + checkTimeStamp(t, now+2, atomic.LoadUint32(&Timestamp)) } func Benchmark_CalculateTimestamp(b *testing.B) { @@ -31,7 +34,7 @@ func Benchmark_CalculateTimestamp(b *testing.B) { var res uint32 b.Run("fiber", func(b *testing.B) { for n := 0; n < b.N; n++ { - res = Timestamp.Load() + res = atomic.LoadUint32(&Timestamp) } checkTimeStamp(b, uint32(time.Now().Unix()), res) }) From 55070c7eeb9455ca86139b2406e8f867864fd294 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Wed, 19 Oct 2022 12:17:46 +0200 Subject: [PATCH 5/6] improve memory storage code and performance --- utils/time_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/utils/time_test.go b/utils/time_test.go index 2f74d55498..75c13b1dd8 100644 --- a/utils/time_test.go +++ b/utils/time_test.go @@ -4,13 +4,11 @@ import ( "sync/atomic" "testing" "time" - - "github.com/stretchr/testify/require" ) func checkTimeStamp(t testing.TB, expectedCurrent, actualCurrent uint32) { // test with some buffer in front and back of the expectedCurrent time -> because of the timing on the work machine - require.Equal(t, true, actualCurrent >= expectedCurrent-1 || actualCurrent <= expectedCurrent+1) + AssertEqual(t, true, actualCurrent >= expectedCurrent-1 || actualCurrent <= expectedCurrent+1) } func Test_TimeStampUpdater(t *testing.T) { From 9524f0120b3136a99485309978d62b8e055011cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Wed, 19 Oct 2022 16:07:45 +0200 Subject: [PATCH 6/6] improve memory storage code and performance --- internal/storage/memory/memory_test.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/internal/storage/memory/memory_test.go b/internal/storage/memory/memory_test.go index 20b398a761..1fc527f2cd 100644 --- a/internal/storage/memory/memory_test.go +++ b/internal/storage/memory/memory_test.go @@ -9,7 +9,7 @@ import ( var testStore = New() -func Test_Memory_Set(t *testing.T) { +func Test_Storage_Memory_Set(t *testing.T) { var ( key = "john" val = []byte("doe") @@ -19,7 +19,7 @@ func Test_Memory_Set(t *testing.T) { utils.AssertEqual(t, nil, err) } -func Test_Memory_Set_Override(t *testing.T) { +func Test_Storage_Memory_Set_Override(t *testing.T) { var ( key = "john" val = []byte("doe") @@ -32,7 +32,7 @@ func Test_Memory_Set_Override(t *testing.T) { utils.AssertEqual(t, nil, err) } -func Test_Memory_Get(t *testing.T) { +func Test_Storage_Memory_Get(t *testing.T) { var ( key = "john" val = []byte("doe") @@ -46,7 +46,7 @@ func Test_Memory_Get(t *testing.T) { utils.AssertEqual(t, val, result) } -func Test_Memory_Set_Expiration(t *testing.T) { +func Test_Storage_Memory_Set_Expiration(t *testing.T) { var ( key = "john" val = []byte("doe") @@ -59,7 +59,7 @@ func Test_Memory_Set_Expiration(t *testing.T) { time.Sleep(1100 * time.Millisecond) } -func Test_Memory_Get_Expired(t *testing.T) { +func Test_Storage_Memory_Get_Expired(t *testing.T) { var ( key = "john" ) @@ -69,14 +69,14 @@ func Test_Memory_Get_Expired(t *testing.T) { utils.AssertEqual(t, true, len(result) == 0) } -func Test_Memory_Get_NotExist(t *testing.T) { +func Test_Storage_Memory_Get_NotExist(t *testing.T) { result, err := testStore.Get("notexist") utils.AssertEqual(t, nil, err) utils.AssertEqual(t, true, len(result) == 0) } -func Test_Memory_Delete(t *testing.T) { +func Test_Storage_Memory_Delete(t *testing.T) { var ( key = "john" val = []byte("doe") @@ -93,7 +93,7 @@ func Test_Memory_Delete(t *testing.T) { utils.AssertEqual(t, true, len(result) == 0) } -func Test_Memory_Reset(t *testing.T) { +func Test_Storage_Memory_Reset(t *testing.T) { var ( val = []byte("doe") ) @@ -116,16 +116,16 @@ func Test_Memory_Reset(t *testing.T) { utils.AssertEqual(t, true, len(result) == 0) } -func Test_Memory_Close(t *testing.T) { +func Test_Storage_Memory_Close(t *testing.T) { utils.AssertEqual(t, nil, testStore.Close()) } -func Test_Memory_Conn(t *testing.T) { +func Test_Storage_Memory_Conn(t *testing.T) { utils.AssertEqual(t, true, testStore.Conn() != nil) } -// go test -v -run=^$ -bench=Benchmark_Memory -benchmem -count=4 -func Benchmark_Memory(b *testing.B) { +// go test -v -run=^$ -bench=Benchmark_Storage_Memory -benchmem -count=4 +func Benchmark_Storage_Memory(b *testing.B) { keyLength := 1000 keys := make([]string, keyLength) for i := 0; i < keyLength; i++ {