mirror of
https://github.com/EchoVault/SugarDB.git
synced 2025-10-05 16:06:57 +08:00
609 lines
20 KiB
Go
609 lines
20 KiB
Go
// Copyright 2024 Kelvin Clement Mwinuka
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package echovault
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/echovault/echovault/internal"
|
|
"github.com/echovault/echovault/internal/constants"
|
|
"github.com/echovault/echovault/internal/eviction"
|
|
"log"
|
|
"math/rand"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// SwapDBs swaps every TCP client connection from database1 over to database2.
|
|
// It also swaps every TCP client connection from database2 over to database1.
|
|
// This only affects TCP connections, it does not swap the logical database currently
|
|
// being used by the embedded API.
|
|
func (server *EchoVault) SwapDBs(database1, database2 int) {
|
|
// If the databases are the same, skip the swap.
|
|
if database1 == database2 {
|
|
return
|
|
}
|
|
|
|
// If any of the databases does not exist, create them.
|
|
server.storeLock.Lock()
|
|
for _, database := range []int{database1, database2} {
|
|
if server.store[database] == nil {
|
|
server.createDatabase(database)
|
|
}
|
|
}
|
|
server.storeLock.Unlock()
|
|
|
|
// Swap the connections for each database.
|
|
server.connInfo.mut.Lock()
|
|
defer server.connInfo.mut.Unlock()
|
|
for connection, info := range server.connInfo.tcpClients {
|
|
switch info.Database {
|
|
case database1:
|
|
server.connInfo.tcpClients[connection] = internal.ConnectionInfo{
|
|
Id: info.Id,
|
|
Name: info.Name,
|
|
Protocol: info.Protocol,
|
|
Database: database2,
|
|
}
|
|
case database2:
|
|
server.connInfo.tcpClients[connection] = internal.ConnectionInfo{
|
|
Id: info.Id,
|
|
Name: info.Name,
|
|
Protocol: info.Protocol,
|
|
Database: database1,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Flush flushes all the data from the database at the specified index.
|
|
// When -1 is passed, all the logical databases are cleared.
|
|
func (server *EchoVault) Flush(database int) {
|
|
server.storeLock.Lock()
|
|
defer server.storeLock.Unlock()
|
|
|
|
server.keysWithExpiry.rwMutex.Lock()
|
|
defer server.keysWithExpiry.rwMutex.Unlock()
|
|
|
|
server.lfuCache.mutex.Lock()
|
|
defer server.lfuCache.mutex.Unlock()
|
|
|
|
server.lruCache.mutex.Lock()
|
|
defer server.lruCache.mutex.Unlock()
|
|
|
|
if database == -1 {
|
|
for db, _ := range server.store {
|
|
// Clear db store.
|
|
clear(server.store[db])
|
|
// Clear db volatile key tracker.
|
|
clear(server.keysWithExpiry.keys[db])
|
|
// Clear db LFU cache.
|
|
server.lfuCache.cache[db] = eviction.NewCacheLFU()
|
|
// Clear db LRU cache.
|
|
server.lruCache.cache[db] = eviction.NewCacheLRU()
|
|
}
|
|
return
|
|
}
|
|
|
|
// Clear db store.
|
|
clear(server.store[database])
|
|
// Clear db volatile key tracker.
|
|
clear(server.keysWithExpiry.keys[database])
|
|
// Clear db LFU cache.
|
|
server.lfuCache.cache[database] = eviction.NewCacheLFU()
|
|
// Clear db LRU cache.
|
|
server.lruCache.cache[database] = eviction.NewCacheLRU()
|
|
}
|
|
|
|
func (server *EchoVault) keysExist(ctx context.Context, keys []string) map[string]bool {
|
|
server.storeLock.RLock()
|
|
defer server.storeLock.RUnlock()
|
|
|
|
database := ctx.Value("Database").(int)
|
|
|
|
exists := make(map[string]bool, len(keys))
|
|
|
|
for _, key := range keys {
|
|
_, ok := server.store[database][key]
|
|
exists[key] = ok
|
|
}
|
|
|
|
return exists
|
|
}
|
|
|
|
func (server *EchoVault) getExpiry(ctx context.Context, key string) time.Time {
|
|
server.storeLock.RLock()
|
|
defer server.storeLock.RUnlock()
|
|
|
|
database := ctx.Value("Database").(int)
|
|
|
|
entry, ok := server.store[database][key]
|
|
if !ok {
|
|
return time.Time{}
|
|
}
|
|
|
|
return entry.ExpireAt
|
|
}
|
|
|
|
func (server *EchoVault) getValues(ctx context.Context, keys []string) map[string]interface{} {
|
|
server.storeLock.Lock()
|
|
defer server.storeLock.Unlock()
|
|
|
|
database := ctx.Value("Database").(int)
|
|
|
|
values := make(map[string]interface{}, len(keys))
|
|
|
|
for _, key := range keys {
|
|
entry, ok := server.store[database][key]
|
|
if !ok {
|
|
values[key] = nil
|
|
continue
|
|
}
|
|
|
|
if entry.ExpireAt != (time.Time{}) && entry.ExpireAt.Before(server.clock.Now()) {
|
|
if !server.isInCluster() {
|
|
// If in standalone mode, delete the key directly.
|
|
err := server.deleteKey(ctx, key)
|
|
if err != nil {
|
|
log.Printf("keyExists: %+v\n", err)
|
|
}
|
|
} else if server.isInCluster() && server.raft.IsRaftLeader() {
|
|
// If we're in a raft cluster, and we're the leader, send command to delete the key in the cluster.
|
|
err := server.raftApplyDeleteKey(ctx, key)
|
|
if err != nil {
|
|
log.Printf("keyExists: %+v\n", err)
|
|
}
|
|
} else if server.isInCluster() && !server.raft.IsRaftLeader() {
|
|
// Forward message to leader to initiate key deletion.
|
|
// This is always called regardless of ForwardCommand config value
|
|
// because we always want to remove expired keys.
|
|
server.memberList.ForwardDeleteKey(ctx, key)
|
|
}
|
|
values[key] = nil
|
|
continue
|
|
}
|
|
|
|
values[key] = entry.Value
|
|
}
|
|
|
|
// Asynchronously update the keys in the cache.
|
|
go func(ctx context.Context, keys []string) {
|
|
if err := server.updateKeysInCache(ctx, keys); err != nil {
|
|
log.Printf("getValues error: %+v\n", err)
|
|
}
|
|
}(ctx, keys)
|
|
|
|
return values
|
|
}
|
|
|
|
func (server *EchoVault) setValues(ctx context.Context, entries map[string]interface{}) error {
|
|
server.storeLock.Lock()
|
|
defer server.storeLock.Unlock()
|
|
|
|
if internal.IsMaxMemoryExceeded(server.config.MaxMemory) && server.config.EvictionPolicy == constants.NoEviction {
|
|
return errors.New("max memory reached, key value not set")
|
|
}
|
|
|
|
database := ctx.Value("Database").(int)
|
|
|
|
// If database does not exist, create it.
|
|
if server.store[database] == nil {
|
|
server.createDatabase(database)
|
|
}
|
|
|
|
for key, value := range entries {
|
|
expireAt := time.Time{}
|
|
if _, ok := server.store[database][key]; ok {
|
|
expireAt = server.store[database][key].ExpireAt
|
|
}
|
|
server.store[database][key] = internal.KeyData{
|
|
Value: value,
|
|
ExpireAt: expireAt,
|
|
}
|
|
if !server.isInCluster() {
|
|
server.snapshotEngine.IncrementChangeCount()
|
|
}
|
|
}
|
|
|
|
// Asynchronously update the keys in the cache.
|
|
go func(ctx context.Context, entries map[string]interface{}) {
|
|
for key, _ := range entries {
|
|
err := server.updateKeysInCache(ctx, []string{key})
|
|
if err != nil {
|
|
log.Printf("setValues error: %+v\n", err)
|
|
}
|
|
}
|
|
}(ctx, entries)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (server *EchoVault) setExpiry(ctx context.Context, key string, expireAt time.Time, touch bool) {
|
|
server.storeLock.Lock()
|
|
defer server.storeLock.Unlock()
|
|
|
|
database := ctx.Value("Database").(int)
|
|
|
|
server.store[database][key] = internal.KeyData{
|
|
Value: server.store[database][key].Value,
|
|
ExpireAt: expireAt,
|
|
}
|
|
|
|
// If the slice of keys associated with expiry time does not contain the current key, add the key.
|
|
server.keysWithExpiry.rwMutex.Lock()
|
|
if !slices.Contains(server.keysWithExpiry.keys[database], key) {
|
|
server.keysWithExpiry.keys[database] = append(server.keysWithExpiry.keys[database], key)
|
|
}
|
|
server.keysWithExpiry.rwMutex.Unlock()
|
|
|
|
// If touch is true, update the keys status in the cache.
|
|
if touch {
|
|
go func(ctx context.Context, key string) {
|
|
err := server.updateKeysInCache(ctx, []string{key})
|
|
if err != nil {
|
|
log.Printf("setExpiry error: %+v\n", err)
|
|
}
|
|
}(ctx, key)
|
|
}
|
|
}
|
|
|
|
func (server *EchoVault) deleteKey(ctx context.Context, key string) error {
|
|
database := ctx.Value("Database").(int)
|
|
|
|
// Delete the key from keyLocks and store.
|
|
delete(server.store[database], key)
|
|
|
|
// Remove key from slice of keys associated with expiry.
|
|
server.keysWithExpiry.rwMutex.Lock()
|
|
defer server.keysWithExpiry.rwMutex.Unlock()
|
|
server.keysWithExpiry.keys[database] = slices.DeleteFunc(server.keysWithExpiry.keys[database], func(k string) bool {
|
|
return k == key
|
|
})
|
|
|
|
// Remove the key from the cache associated with the database.
|
|
switch {
|
|
case slices.Contains([]string{constants.AllKeysLFU, constants.VolatileLFU}, server.config.EvictionPolicy):
|
|
server.lfuCache.cache[database].Delete(key)
|
|
case slices.Contains([]string{constants.AllKeysLRU, constants.VolatileLRU}, server.config.EvictionPolicy):
|
|
server.lruCache.cache[database].Delete(key)
|
|
}
|
|
|
|
log.Printf("deleted key %s\n", key)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (server *EchoVault) createDatabase(database int) {
|
|
// Create database store.
|
|
server.store[database] = make(map[string]internal.KeyData)
|
|
|
|
// Set volatile keys tracker for database.
|
|
server.keysWithExpiry.rwMutex.Lock()
|
|
defer server.keysWithExpiry.rwMutex.Unlock()
|
|
server.keysWithExpiry.keys[database] = make([]string, 0)
|
|
|
|
// Create database LFU cache.
|
|
server.lfuCache.mutex.Lock()
|
|
defer server.lfuCache.mutex.Unlock()
|
|
server.lfuCache.cache[database] = eviction.NewCacheLFU()
|
|
|
|
// Create database LRU cache.
|
|
server.lruCache.mutex.Lock()
|
|
defer server.lruCache.mutex.Unlock()
|
|
server.lruCache.cache[database] = eviction.NewCacheLRU()
|
|
}
|
|
|
|
func (server *EchoVault) getState() map[int]map[string]interface{} {
|
|
// Wait unit there's no state mutation or copy in progress before starting a new copy process.
|
|
for {
|
|
if !server.stateCopyInProgress.Load() && !server.stateMutationInProgress.Load() {
|
|
server.stateCopyInProgress.Store(true)
|
|
break
|
|
}
|
|
}
|
|
data := make(map[int]map[string]interface{})
|
|
for db, store := range server.store {
|
|
data[db] = make(map[string]interface{})
|
|
for k, v := range store {
|
|
data[db][k] = v
|
|
}
|
|
}
|
|
server.stateCopyInProgress.Store(false)
|
|
return data
|
|
}
|
|
|
|
// updateKeysInCache updates either the key access count or the most recent access time in the cache
|
|
// depending on whether an LFU or LRU strategy was used.
|
|
func (server *EchoVault) updateKeysInCache(ctx context.Context, keys []string) error {
|
|
database := ctx.Value("Database").(int)
|
|
for _, key := range keys {
|
|
// Only update cache when in standalone mode or when raft leader.
|
|
if server.isInCluster() || (server.isInCluster() && !server.raft.IsRaftLeader()) {
|
|
return nil
|
|
}
|
|
// If max memory is 0, there's no max so no need to update caches.
|
|
if server.config.MaxMemory == 0 {
|
|
return nil
|
|
}
|
|
switch strings.ToLower(server.config.EvictionPolicy) {
|
|
case constants.AllKeysLFU:
|
|
server.lfuCache.mutex.Lock()
|
|
server.lfuCache.cache[database].Update(key)
|
|
server.lfuCache.mutex.Unlock()
|
|
case constants.AllKeysLRU:
|
|
server.lruCache.mutex.Lock()
|
|
server.lruCache.cache[database].Update(key)
|
|
server.lruCache.mutex.Unlock()
|
|
case constants.VolatileLFU:
|
|
server.lfuCache.mutex.Lock()
|
|
if server.store[database][key].ExpireAt != (time.Time{}) {
|
|
server.lfuCache.cache[database].Update(key)
|
|
}
|
|
server.lfuCache.mutex.Unlock()
|
|
case constants.VolatileLRU:
|
|
server.lruCache.mutex.Lock()
|
|
if server.store[database][key].ExpireAt != (time.Time{}) {
|
|
server.lruCache.cache[database].Update(key)
|
|
}
|
|
server.lruCache.mutex.Unlock()
|
|
}
|
|
}
|
|
|
|
// TODO: Adjust memory by taking all databases into account (largest database?).
|
|
//if err := server.adjustMemoryUsage(ctx); err != nil {
|
|
// return fmt.Errorf("updateKeysInCache: %+v", err)
|
|
//}
|
|
return nil
|
|
}
|
|
|
|
// TODO: Implement support for multiple databases.
|
|
// adjustMemoryUsage should only be called from standalone echovault or from raft cluster leader.
|
|
//func (server *EchoVault) adjustMemoryUsage(ctx context.Context) error {
|
|
// // If max memory is 0, there's no need to adjust memory usage.
|
|
// if server.config.MaxMemory == 0 {
|
|
// return nil
|
|
// }
|
|
// // Check if memory usage is above max-memory.
|
|
// // If it is, pop items from the cache until we get under the limit.
|
|
// var memStats runtime.MemStats
|
|
// runtime.ReadMemStats(&memStats)
|
|
// // If we're using less memory than the max-memory, there's no need to evict.
|
|
// if memStats.HeapInuse < server.config.MaxMemory {
|
|
// return nil
|
|
// }
|
|
// // Force a garbage collection first before we start evicting keys.
|
|
// runtime.GC()
|
|
// runtime.ReadMemStats(&memStats)
|
|
// if memStats.HeapInuse < server.config.MaxMemory {
|
|
// return nil
|
|
// }
|
|
// // We've done a GC, but we're still at or above the max memory limit.
|
|
// // Start a loop that evicts keys until either the heap is empty or
|
|
// // we're below the max memory limit.
|
|
// server.storeLock.Lock()
|
|
// defer server.storeLock.Unlock()
|
|
// switch {
|
|
// case slices.Contains([]string{constants.AllKeysLFU, constants.VolatileLFU}, strings.ToLower(server.config.EvictionPolicy)):
|
|
// // Remove keys from LFU cache until we're below the max memory limit or
|
|
// // until the LFU cache is empty.
|
|
// server.lfuCache.mutex.Lock()
|
|
// defer server.lfuCache.mutex.Unlock()
|
|
// for {
|
|
// // Return if cache is empty
|
|
// if server.lfuCache.cache.Len() == 0 {
|
|
// return fmt.Errorf("adjustMemoryUsage -> LFU cache empty")
|
|
// }
|
|
//
|
|
// key := heap.Pop(&server.lfuCache.cache).(string)
|
|
// if !server.isInCluster() {
|
|
// // If in standalone mode, directly delete the key
|
|
// if err := server.deleteKey(key); err != nil {
|
|
// return fmt.Errorf("adjustMemoryUsage -> LFU cache eviction: %+v", err)
|
|
// }
|
|
// } else if server.isInCluster() && server.raft.IsRaftLeader() {
|
|
// // If in raft cluster, send command to delete key from cluster
|
|
// if err := server.raftApplyDeleteKey(ctx, key); err != nil {
|
|
// return fmt.Errorf("adjustMemoryUsage -> LFU cache eviction: %+v", err)
|
|
// }
|
|
// }
|
|
//
|
|
// // Run garbage collection
|
|
// runtime.GC()
|
|
// // Return if we're below max memory
|
|
// runtime.ReadMemStats(&memStats)
|
|
// if memStats.HeapInuse < server.config.MaxMemory {
|
|
// return nil
|
|
// }
|
|
// }
|
|
// case slices.Contains([]string{constants.AllKeysLRU, constants.VolatileLRU}, strings.ToLower(server.config.EvictionPolicy)):
|
|
// // Remove keys from th LRU cache until we're below the max memory limit or
|
|
// // until the LRU cache is empty.
|
|
// server.lruCache.mutex.Lock()
|
|
// defer server.lruCache.mutex.Unlock()
|
|
// for {
|
|
// // Return if cache is empty
|
|
// if server.lruCache.cache.Len() == 0 {
|
|
// return fmt.Errorf("adjsutMemoryUsage -> LRU cache empty")
|
|
// }
|
|
//
|
|
// key := heap.Pop(&server.lruCache.cache).(string)
|
|
// if !server.isInCluster() {
|
|
// // If in standalone mode, directly delete the key.
|
|
// if err := server.deleteKey(key); err != nil {
|
|
// return fmt.Errorf("adjustMemoryUsage -> LRU cache eviction: %+v", err)
|
|
// }
|
|
// } else if server.isInCluster() && server.raft.IsRaftLeader() {
|
|
// // If in cluster mode and the node is a cluster leader,
|
|
// // send command to delete the key from the cluster.
|
|
// if err := server.raftApplyDeleteKey(ctx, key); err != nil {
|
|
// return fmt.Errorf("adjustMemoryUsage -> LRU cache eviction: %+v", err)
|
|
// }
|
|
// }
|
|
//
|
|
// // Run garbage collection
|
|
// runtime.GC()
|
|
// // Return if we're below max memory
|
|
// runtime.ReadMemStats(&memStats)
|
|
// if memStats.HeapInuse < server.config.MaxMemory {
|
|
// return nil
|
|
// }
|
|
// }
|
|
// case slices.Contains([]string{constants.AllKeysRandom}, strings.ToLower(server.config.EvictionPolicy)):
|
|
// // Remove random keys until we're below the max memory limit
|
|
// // or there are no more keys remaining.
|
|
// for {
|
|
// server.storeLock.Lock()
|
|
// // If there are no keys, return error
|
|
// if len(server.store) == 0 {
|
|
// err := errors.New("no keys to evict")
|
|
// server.storeLock.Unlock()
|
|
// return fmt.Errorf("adjustMemoryUsage -> all keys random: %+v", err)
|
|
// }
|
|
// // Get random key
|
|
// idx := rand.Intn(len(server.store))
|
|
// for key, _ := range server.store {
|
|
// if idx == 0 {
|
|
// if !server.isInCluster() {
|
|
// // If in standalone mode, directly delete the key
|
|
// if err := server.deleteKey(key); err != nil {
|
|
// return fmt.Errorf("adjustMemoryUsage -> all keys random: %+v", err)
|
|
// }
|
|
// } else if server.isInCluster() && server.raft.IsRaftLeader() {
|
|
// if err := server.raftApplyDeleteKey(ctx, key); err != nil {
|
|
// return fmt.Errorf("adjustMemoryUsage -> all keys random: %+v", err)
|
|
// }
|
|
// }
|
|
// // Run garbage collection
|
|
// runtime.GC()
|
|
// // Return if we're below max memory
|
|
// runtime.ReadMemStats(&memStats)
|
|
// if memStats.HeapInuse < server.config.MaxMemory {
|
|
// return nil
|
|
// }
|
|
// }
|
|
// idx--
|
|
// }
|
|
// }
|
|
// case slices.Contains([]string{constants.VolatileRandom}, strings.ToLower(server.config.EvictionPolicy)):
|
|
// // Remove random keys with an associated expiry time until we're below the max memory limit
|
|
// // or there are no more keys with expiry time.
|
|
// for {
|
|
// // Get random volatile key
|
|
// server.keysWithExpiry.rwMutex.RLock()
|
|
// idx := rand.Intn(len(server.keysWithExpiry.keys))
|
|
// key := server.keysWithExpiry.keys[idx]
|
|
// server.keysWithExpiry.rwMutex.RUnlock()
|
|
//
|
|
// if !server.isInCluster() {
|
|
// // If in standalone mode, directly delete the key
|
|
// if err := server.deleteKey(key); err != nil {
|
|
// return fmt.Errorf("adjustMemoryUsage -> volatile keys random: %+v", err)
|
|
// }
|
|
// } else if server.isInCluster() && server.raft.IsRaftLeader() {
|
|
// if err := server.raftApplyDeleteKey(ctx, key); err != nil {
|
|
// return fmt.Errorf("adjustMemoryUsage -> volatile keys randome: %+v", err)
|
|
// }
|
|
// }
|
|
//
|
|
// // Run garbage collection
|
|
// runtime.GC()
|
|
// // Return if we're below max memory
|
|
// runtime.ReadMemStats(&memStats)
|
|
// if memStats.HeapInuse < server.config.MaxMemory {
|
|
// return nil
|
|
// }
|
|
// }
|
|
// default:
|
|
// return nil
|
|
// }
|
|
// }
|
|
|
|
// evictKeysWithExpiredTTL is a function that samples keys with an associated TTL
|
|
// and evicts keys that are currently expired.
|
|
// This function will sample 20 keys from the list of keys with an associated TTL,
|
|
// if the key is expired, it will be evicted.
|
|
// This function is only executed in standalone mode or by the raft cluster leader.
|
|
func (server *EchoVault) evictKeysWithExpiredTTL(ctx context.Context) error {
|
|
// Only execute this if we're in standalone mode, or raft cluster leader.
|
|
if server.isInCluster() && !server.raft.IsRaftLeader() {
|
|
return nil
|
|
}
|
|
|
|
server.keysWithExpiry.rwMutex.RLock()
|
|
|
|
database := ctx.Value("Database").(int)
|
|
|
|
// Sample size should be the configured sample size, or the size of the keys with expiry,
|
|
// whichever one is smaller.
|
|
sampleSize := int(server.config.EvictionSample)
|
|
if len(server.keysWithExpiry.keys[database]) < sampleSize {
|
|
sampleSize = len(server.keysWithExpiry.keys)
|
|
}
|
|
keys := make([]string, sampleSize)
|
|
|
|
deletedCount := 0
|
|
thresholdPercentage := 20
|
|
|
|
var idx int
|
|
var key string
|
|
for i := 0; i < len(keys); i++ {
|
|
for {
|
|
// Retry retrieval of a random key until we find a key that is not already in the list of sampled keys.
|
|
idx = rand.Intn(len(server.keysWithExpiry.keys))
|
|
key = server.keysWithExpiry.keys[database][idx]
|
|
if !slices.Contains(keys, key) {
|
|
keys[i] = key
|
|
break
|
|
}
|
|
}
|
|
}
|
|
server.keysWithExpiry.rwMutex.RUnlock()
|
|
|
|
// Loop through the keys and delete them if they're expired
|
|
server.storeLock.Lock()
|
|
defer server.storeLock.Unlock()
|
|
for _, k := range keys {
|
|
// Delete the expired key
|
|
deletedCount += 1
|
|
if !server.isInCluster() {
|
|
if err := server.deleteKey(ctx, k); err != nil {
|
|
return fmt.Errorf("evictKeysWithExpiredTTL -> standalone delete: %+v", err)
|
|
}
|
|
} else if server.isInCluster() && server.raft.IsRaftLeader() {
|
|
if err := server.raftApplyDeleteKey(ctx, k); err != nil {
|
|
return fmt.Errorf("evictKeysWithExpiredTTL -> cluster delete: %+v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// If sampleSize is 0, there's no need to calculate deleted percentage.
|
|
if sampleSize == 0 {
|
|
return nil
|
|
}
|
|
|
|
log.Printf("%d keys sampled, %d keys deleted\n", sampleSize, deletedCount)
|
|
|
|
// If the deleted percentage is over 20% of the sample size, execute the function again immediately.
|
|
if (deletedCount/sampleSize)*100 >= thresholdPercentage {
|
|
log.Printf("deletion ratio (%d percent) reached threshold (%d percent), sampling again\n",
|
|
(deletedCount/sampleSize)*100, thresholdPercentage)
|
|
return server.evictKeysWithExpiredTTL(ctx)
|
|
}
|
|
|
|
return nil
|
|
}
|