From e472d91a926875830ccad8959f6f9906755e0e68 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 28 Jan 2025 11:56:15 -0700 Subject: [PATCH 01/82] Added MaxDatabases option --- libs/host/Configuration/Options.cs | 4 ++++ libs/host/Configuration/Redis/RedisOptions.cs | 3 +++ libs/host/defaults.conf | 5 ++++- test/Garnet.test/GarnetServerConfigTests.cs | 4 +++- test/Garnet.test/redis.conf | 2 +- 5 files changed, 15 insertions(+), 3 deletions(-) diff --git a/libs/host/Configuration/Options.cs b/libs/host/Configuration/Options.cs index 82a8b32f13..4f64c61262 100644 --- a/libs/host/Configuration/Options.cs +++ b/libs/host/Configuration/Options.cs @@ -525,6 +525,10 @@ internal sealed class Options [Option("lua-script-memory-limit", Default = null, HelpText = "Memory limit for a Lua instances while running a script, lua-memory-management-mode must be set to something other than Native to use this flag")] public string LuaScriptMemoryLimit { get; set; } + [IntRangeValidation(1, 256, isRequired: false)] + [Option("max-databases", Required = false, HelpText = "Max number of logical databases allowed in a single Garnet server instance")] + public int MaxDatabases { get; set; } + /// /// This property contains all arguments that were not parsed by the command line argument parser /// diff --git a/libs/host/Configuration/Redis/RedisOptions.cs b/libs/host/Configuration/Redis/RedisOptions.cs index 48c696e21d..1f4c13c4d0 100644 --- a/libs/host/Configuration/Redis/RedisOptions.cs +++ b/libs/host/Configuration/Redis/RedisOptions.cs @@ -82,6 +82,9 @@ internal class RedisOptions [RedisOption("repl-diskless-sync-delay", nameof(Options.ReplicaSyncDelayMs))] public Option ReplicaDisklessSyncDelay { get; set; } + + [RedisOption("databases", nameof(Options.MaxDatabases))] + public Option Databases { get; set; } } /// diff --git a/libs/host/defaults.conf b/libs/host/defaults.conf index 01c31e5c3f..689dbf6528 100644 --- a/libs/host/defaults.conf +++ b/libs/host/defaults.conf @@ -354,5 +354,8 @@ "LuaMemoryManagementMode": "Native", /* Lua limits are ignored for Native, but can be set for other modes */ - "LuaScriptMemoryLimit": null + "LuaScriptMemoryLimit": null, + + /* Max number of logical databases allowed in a single Garnet server instance */ + "MaxDatabases": 16 } \ No newline at end of file diff --git a/test/Garnet.test/GarnetServerConfigTests.cs b/test/Garnet.test/GarnetServerConfigTests.cs index 2890af40b2..297b240ba6 100644 --- a/test/Garnet.test/GarnetServerConfigTests.cs +++ b/test/Garnet.test/GarnetServerConfigTests.cs @@ -189,10 +189,11 @@ public void ImportExportRedisConfigLocal() ClassicAssert.IsTrue(options.ClientCertificateRequired); ClassicAssert.AreEqual("testcert.pfx", options.CertFileName); ClassicAssert.AreEqual("placeholder", options.CertPassword); + ClassicAssert.AreEqual(32, options.MaxDatabases); // Import from redis.conf file, include command line args // Check values from import path override values from default.conf, and values from command line override values from default.conf and import path - args = ["--config-import-path", redisConfigPath, "--config-import-format", "RedisConf", "--config-export-path", garnetConfigPath, "-p", "12m", "--tls", "false", "--minthreads", "6", "--client-certificate-required", "true"]; + args = ["--config-import-path", redisConfigPath, "--config-import-format", "RedisConf", "--config-export-path", garnetConfigPath, "-p", "12m", "--tls", "false", "--minthreads", "6", "--client-certificate-required", "true", "--max-databases", "64"]; parseSuccessful = ServerSettingsManager.TryParseCommandLineArguments(args, out options, out invalidOptions, out exitGracefully); ClassicAssert.IsTrue(parseSuccessful); ClassicAssert.AreEqual(invalidOptions.Count, 0); @@ -203,6 +204,7 @@ public void ImportExportRedisConfigLocal() ClassicAssert.AreEqual(5, options.ReplicaSyncDelayMs); ClassicAssert.IsFalse(options.EnableTLS); ClassicAssert.IsTrue(options.ClientCertificateRequired); + ClassicAssert.AreEqual(64, options.MaxDatabases); ClassicAssert.IsTrue(File.Exists(garnetConfigPath)); TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); diff --git a/test/Garnet.test/redis.conf b/test/Garnet.test/redis.conf index d854fc6e3e..3bf5c5e904 100644 --- a/test/Garnet.test/redis.conf +++ b/test/Garnet.test/redis.conf @@ -377,7 +377,7 @@ logfile ./garnet-log # Set the number of databases. The default database is DB 0, you can select # a different one on a per-connection basis using SELECT where # dbid is a number between 0 and 'databases'-1 -# databases 16 +databases 32 # By default Redis shows an ASCII art logo only when started to log to the # standard output and if the standard output is a TTY and syslog logging is From ba2f091147f780f0e817f3d58b45a96b77e2d1b6 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 28 Jan 2025 17:00:02 -0700 Subject: [PATCH 02/82] wip --- libs/host/Configuration/Options.cs | 1 + libs/host/GarnetServer.cs | 125 +++++++--------- libs/server/AOF/AofProcessor.cs | 4 +- libs/server/Custom/ExpandableMap.cs | 41 +++++ libs/server/GarnetDatabase.cs | 79 ++++++++++ libs/server/Resp/ArrayCommands.cs | 2 +- libs/server/Resp/BasicCommands.cs | 10 +- libs/server/Resp/GarnetDatabaseSession.cs | 55 +++++++ libs/server/Resp/RespServerSession.cs | 57 ++++++- libs/server/ServerConfig.cs | 2 +- libs/server/Servers/GarnetServerOptions.cs | 21 ++- libs/server/Servers/StoreApi.cs | 7 +- libs/server/Storage/Session/StorageSession.cs | 16 +- libs/server/StoreWrapper.cs | 141 ++++++++++++++---- 14 files changed, 437 insertions(+), 124 deletions(-) create mode 100644 libs/server/GarnetDatabase.cs create mode 100644 libs/server/Resp/GarnetDatabaseSession.cs diff --git a/libs/host/Configuration/Options.cs b/libs/host/Configuration/Options.cs index 4f64c61262..a50e35d5c8 100644 --- a/libs/host/Configuration/Options.cs +++ b/libs/host/Configuration/Options.cs @@ -743,6 +743,7 @@ public GarnetServerOptions GetServerOptions(ILogger logger = null) FailOnRecoveryError = FailOnRecoveryError.GetValueOrDefault(), SkipRDBRestoreChecksumValidation = SkipRDBRestoreChecksumValidation.GetValueOrDefault(), LuaOptions = EnableLua.GetValueOrDefault() ? new LuaOptions(LuaMemoryManagementMode, LuaScriptMemoryLimit, logger) : null, + MaxDatabases = MaxDatabases, }; } diff --git a/libs/host/GarnetServer.cs b/libs/host/GarnetServer.cs index c4af9b8dfe..c91a5239ea 100644 --- a/libs/host/GarnetServer.cs +++ b/libs/host/GarnetServer.cs @@ -40,10 +40,6 @@ static string GetVersion() private readonly GarnetServerOptions opts; private IGarnetServer server; - private TsavoriteKV store; - private TsavoriteKV objectStore; - private IDevice aofDevice; - private TsavoriteLog appendOnlyFile; private SubscribeBroker> subscribeBroker; private KVSettings kvSettings; private KVSettings objKvSettings; @@ -224,29 +220,25 @@ private void InitializeServer() if (!setMax && !ThreadPool.SetMaxThreads(maxThreads, maxCPThreads)) throw new Exception($"Unable to call ThreadPool.SetMaxThreads with {maxThreads}, {maxCPThreads}"); - CreateMainStore(clusterFactory, out var checkpointDir); - CreateObjectStore(clusterFactory, customCommandManager, checkpointDir, out var objectStoreSizeTracker); + var createDatabaseDelegate = (int dbId) => + { + var store = CreateMainStore(dbId, clusterFactory, out var currCheckpointDir); + var objectStore = CreateObjectStore(dbId, clusterFactory, customCommandManager, currCheckpointDir, + out var objectStoreSizeTracker); + var (aofDevice, aof) = CreateAOF(dbId); + return new GarnetDatabase(store, objectStore, objectStoreSizeTracker, aofDevice, aof); + }; if (!opts.DisablePubSub) subscribeBroker = new SubscribeBroker>(new SpanByteKeySerializer(), null, opts.PubSubPageSizeBytes(), opts.SubscriberRefreshFrequencyMs, true); - CreateAOF(); - logger?.LogTrace("TLS is {tlsEnabled}", opts.TlsOptions == null ? "disabled" : "enabled"); - if (logger != null) - { - var configMemoryLimit = (store.IndexSize * 64) + store.Log.MaxMemorySizeBytes + (store.ReadCache?.MaxMemorySizeBytes ?? 0) + (appendOnlyFile?.MaxMemorySizeBytes ?? 0); - if (objectStore != null) - configMemoryLimit += objectStore.IndexSize * 64 + objectStore.Log.MaxMemorySizeBytes + (objectStore.ReadCache?.MaxMemorySizeBytes ?? 0) + (objectStoreSizeTracker?.TargetSize ?? 0) + (objectStoreSizeTracker?.ReadCacheTargetSize ?? 0); - logger.LogInformation("Total configured memory limit: {configMemoryLimit}", configMemoryLimit); - } - // Create Garnet TCP server if none was provided. this.server ??= new GarnetServerTcp(opts.Address, opts.Port, 0, opts.TlsOptions, opts.NetworkSendThrottleMax, opts.NetworkConnectionLimit, logger); - storeWrapper = new StoreWrapper(version, redisProtocolVersion, server, store, objectStore, objectStoreSizeTracker, - customCommandManager, appendOnlyFile, opts, clusterFactory: clusterFactory, loggerFactory: loggerFactory); + storeWrapper = new StoreWrapper(version, redisProtocolVersion, server, createDatabaseDelegate, + customCommandManager, opts, clusterFactory: clusterFactory, loggerFactory: loggerFactory); // Create session provider for Garnet Provider = new GarnetProvider(storeWrapper, subscribeBroker); @@ -285,7 +277,7 @@ private void LoadModules(CustomCommandManager customCommandManager) } } - private void CreateMainStore(IClusterFactory clusterFactory, out string checkpointDir) + private TsavoriteKV CreateMainStore(int dbId, IClusterFactory clusterFactory, out string checkpointDir) { kvSettings = opts.GetSettings(loggerFactory, out logFactory); @@ -296,74 +288,73 @@ private void CreateMainStore(IClusterFactory clusterFactory, out string checkpoi kvSettings.CheckpointVersionSwitchBarrier = opts.EnableCluster; var checkpointFactory = opts.DeviceFactoryCreator(); - if (opts.EnableCluster) - { - kvSettings.CheckpointManager = clusterFactory.CreateCheckpointManager(checkpointFactory, - new DefaultCheckpointNamingScheme(checkpointDir + "/Store/checkpoints"), isMainStore: true, logger); - } - else - { - kvSettings.CheckpointManager = new DeviceLogCommitCheckpointManager(checkpointFactory, - new DefaultCheckpointNamingScheme(checkpointDir + "/Store/checkpoints"), removeOutdated: true); - } + var baseName = Path.Combine(checkpointDir, "Store", $"checkpoints{(dbId == 0 ? string.Empty : $"_{dbId}")}"); + var defaultNamingScheme = new DefaultCheckpointNamingScheme(baseName); - store = new(kvSettings + kvSettings.CheckpointManager = opts.EnableCluster ? + clusterFactory.CreateCheckpointManager(checkpointFactory, defaultNamingScheme, isMainStore: true, logger) : + new DeviceLogCommitCheckpointManager(checkpointFactory, defaultNamingScheme, removeOutdated: true); + + return new TsavoriteKV(kvSettings , StoreFunctions.Create() , (allocatorSettings, storeFunctions) => new(allocatorSettings, storeFunctions)); } - private void CreateObjectStore(IClusterFactory clusterFactory, CustomCommandManager customCommandManager, string CheckpointDir, out CacheSizeTracker objectStoreSizeTracker) + private TsavoriteKV CreateObjectStore(int dbId, IClusterFactory clusterFactory, CustomCommandManager customCommandManager, string checkpointDir, out CacheSizeTracker objectStoreSizeTracker) { objectStoreSizeTracker = null; - if (!opts.DisableObjects) - { - objKvSettings = opts.GetObjectStoreSettings(this.loggerFactory?.CreateLogger("TsavoriteKV [obj]"), - out var objHeapMemorySize, out var objReadCacheHeapMemorySize); - - // Run checkpoint on its own thread to control p99 - objKvSettings.ThrottleCheckpointFlushDelayMs = opts.CheckpointThrottleFlushDelayMs; - objKvSettings.CheckpointVersionSwitchBarrier = opts.EnableCluster; - - if (opts.EnableCluster) - objKvSettings.CheckpointManager = clusterFactory.CreateCheckpointManager( - opts.DeviceFactoryCreator(), - new DefaultCheckpointNamingScheme(CheckpointDir + "/ObjectStore/checkpoints"), - isMainStore: false, logger); - else - objKvSettings.CheckpointManager = new DeviceLogCommitCheckpointManager(opts.DeviceFactoryCreator(), - new DefaultCheckpointNamingScheme(CheckpointDir + "/ObjectStore/checkpoints"), - removeOutdated: true); - - objectStore = new(objKvSettings - , StoreFunctions.Create(new ByteArrayKeyComparer(), - () => new ByteArrayBinaryObjectSerializer(), - () => new GarnetObjectSerializer(customCommandManager)) - , (allocatorSettings, storeFunctions) => new(allocatorSettings, storeFunctions)); - - if (objHeapMemorySize > 0 || objReadCacheHeapMemorySize > 0) - objectStoreSizeTracker = new CacheSizeTracker(objectStore, objKvSettings, objHeapMemorySize, objReadCacheHeapMemorySize, - this.loggerFactory); - } + if (opts.DisableObjects) + return null; + + objKvSettings = opts.GetObjectStoreSettings(this.loggerFactory?.CreateLogger("TsavoriteKV [obj]"), + out var objHeapMemorySize, out var objReadCacheHeapMemorySize); + + // Run checkpoint on its own thread to control p99 + objKvSettings.ThrottleCheckpointFlushDelayMs = opts.CheckpointThrottleFlushDelayMs; + objKvSettings.CheckpointVersionSwitchBarrier = opts.EnableCluster; + + var checkpointFactory = opts.DeviceFactoryCreator(); + var baseName = Path.Combine(checkpointDir, "ObjectStore", $"checkpoints{(dbId == 0 ? string.Empty : $"_{dbId}")}"); + var defaultNamingScheme = new DefaultCheckpointNamingScheme(baseName); + + objKvSettings.CheckpointManager = opts.EnableCluster ? + clusterFactory.CreateCheckpointManager(checkpointFactory, defaultNamingScheme, isMainStore: false, logger) : + new DeviceLogCommitCheckpointManager(checkpointFactory, defaultNamingScheme, removeOutdated: true); + + var objStore = new TsavoriteKV( + objKvSettings, + StoreFunctions.Create(new ByteArrayKeyComparer(), + () => new ByteArrayBinaryObjectSerializer(), + () => new GarnetObjectSerializer(customCommandManager)), + (allocatorSettings, storeFunctions) => new(allocatorSettings, storeFunctions)); + + if (objHeapMemorySize > 0 || objReadCacheHeapMemorySize > 0) + objectStoreSizeTracker = new CacheSizeTracker(objStore, objKvSettings, objHeapMemorySize, objReadCacheHeapMemorySize, + this.loggerFactory); + + return objStore; + } - private void CreateAOF() + private (IDevice, TsavoriteLog) CreateAOF(int dbId) { if (opts.EnableAOF) { if (opts.MainMemoryReplication && opts.CommitFrequencyMs != -1) throw new Exception("Need to set CommitFrequencyMs to -1 (manual commits) with MainMemoryReplication"); - opts.GetAofSettings(out var aofSettings); - aofDevice = aofSettings.LogDevice; - appendOnlyFile = new TsavoriteLog(aofSettings, logger: this.loggerFactory?.CreateLogger("TsavoriteLog [aof]")); + opts.GetAofSettings(dbId, out var aofSettings); + var aofDevice = aofSettings.LogDevice; + var appendOnlyFile = new TsavoriteLog(aofSettings, logger: this.loggerFactory?.CreateLogger("TsavoriteLog [aof]")); if (opts.CommitFrequencyMs < 0 && opts.WaitForCommit) throw new Exception("Cannot use CommitWait with manual commits"); - return; + return (aofDevice, appendOnlyFile); } if (opts.CommitFrequencyMs != 0 || opts.WaitForCommit) throw new Exception("Cannot use CommitFrequencyMs or CommitWait without EnableAOF"); + return (null, null); } /// @@ -410,13 +401,9 @@ private void InternalDispose() Provider?.Dispose(); server.Dispose(); subscribeBroker?.Dispose(); - store.Dispose(); - appendOnlyFile?.Dispose(); - aofDevice?.Dispose(); kvSettings.LogDevice?.Dispose(); if (!opts.DisableObjects) { - objectStore.Dispose(); objKvSettings.LogDevice?.Dispose(); objKvSettings.ObjectLogDevice?.Dispose(); } diff --git a/libs/server/AOF/AofProcessor.cs b/libs/server/AOF/AofProcessor.cs index 2a916e3689..b7e1db75db 100644 --- a/libs/server/AOF/AofProcessor.cs +++ b/libs/server/AOF/AofProcessor.cs @@ -72,9 +72,7 @@ public AofProcessor( storeWrapper.version, storeWrapper.redisProtocolVersion, null, - storeWrapper.store, - storeWrapper.objectStore, - storeWrapper.objectStoreSizeTracker, + storeWrapper.createDatabasesDelegate, storeWrapper.customCommandManager, recordToAof ? storeWrapper.appendOnlyFile : null, storeWrapper.serverOptions, diff --git a/libs/server/Custom/ExpandableMap.cs b/libs/server/Custom/ExpandableMap.cs index 3960f4fb01..0303142460 100644 --- a/libs/server/Custom/ExpandableMap.cs +++ b/libs/server/Custom/ExpandableMap.cs @@ -76,6 +76,47 @@ public bool TryGetValue(int id, out T value) return true; } + /// + /// If value exists for specified ID, return it, + /// otherwise set it using the provided value factory + /// + /// Item ID + /// Value factory + /// Returned value + /// True if item was not previously set, but was set by this method + /// True if item was previously initialized or set successfully + public bool TryGetOrSet(int id, Func valueFactory, out T value, out bool added) + { + added = false; + + // Try to get the current value, if value is already set, return it + if (this.TryGetValue(id, out var currValue) && !currValue.Equals(default(T))) + { + value = currValue; + return true; + } + + mapLock.WriteLock(); + try + { + // Try to get the current value, if value is already set, return it + if (this.TryGetValue(id, out currValue) && !currValue.Equals(default(T))) + { + value = currValue; + return true; + } + + // Try to set value with expanding the map, if needed + value = valueFactory(); + added = this.TrySetValueUnsafe(id, ref value, noExpansion: false); + return added; + } + finally + { + mapLock.WriteUnlock(); + } + } + /// /// Try to set item by ID /// diff --git a/libs/server/GarnetDatabase.cs b/libs/server/GarnetDatabase.cs new file mode 100644 index 0000000000..36ee67cbab --- /dev/null +++ b/libs/server/GarnetDatabase.cs @@ -0,0 +1,79 @@ +using System; +using Tsavorite.core; + +namespace Garnet.server +{ + using MainStoreAllocator = SpanByteAllocator>; + using MainStoreFunctions = StoreFunctions; + + using ObjectStoreAllocator = GenericAllocator>>; + using ObjectStoreFunctions = StoreFunctions>; + + /// + /// Represents a logical database in Garnet + /// + public struct GarnetDatabase : IDisposable + { + /// + /// Default size for version map + /// + // TODO: Change map size to a reasonable number + const int DefaultVersionMapSize = 1 << 16; + + /// + /// Main Store + /// + public TsavoriteKV MainStore; + + /// + /// Object Store + /// + public TsavoriteKV ObjectStore; + + /// + /// Size Tracker for Object Store + /// + public CacheSizeTracker ObjectSizeTracker; + + /// + /// Device used for AOF logging + /// + public IDevice AofDevice; + + /// + /// AOF log + /// + public TsavoriteLog AppendOnlyFile; + + /// + /// Version map + /// + public WatchVersionMap VersionMap; + + bool disposed = false; + + public GarnetDatabase(TsavoriteKV mainStore, + TsavoriteKV objectStore, + CacheSizeTracker objectSizeTracker, IDevice aofDevice, TsavoriteLog appendOnlyFile) + { + MainStore = mainStore; + ObjectStore = objectStore; + ObjectSizeTracker = objectSizeTracker; + AofDevice = aofDevice; + AppendOnlyFile = appendOnlyFile; + VersionMap = new WatchVersionMap(DefaultVersionMapSize); + } + + public void Dispose() + { + if (disposed) return; + + MainStore?.Dispose(); + ObjectStore?.Dispose(); + AofDevice?.Dispose(); + AppendOnlyFile?.Dispose(); + + disposed = true; + } + } +} diff --git a/libs/server/Resp/ArrayCommands.cs b/libs/server/Resp/ArrayCommands.cs index 5a247e0c3c..0291925f1a 100644 --- a/libs/server/Resp/ArrayCommands.cs +++ b/libs/server/Resp/ArrayCommands.cs @@ -235,7 +235,7 @@ private bool NetworkSELECT() return true; } - if (index < storeWrapper.databaseNum) + if (index < storeWrapper.databaseCount) { while (!RespWriteUtils.TryWriteDirect(CmdStrings.RESP_OK, ref dcurr, dend)) SendAndReset(); diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index 06a540d057..ea4ca8d081 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -13,6 +13,11 @@ namespace Garnet.server { + using MainStoreAllocator = SpanByteAllocator>; + using MainStoreFunctions = StoreFunctions; + using ObjectStoreAllocator = GenericAllocator>>; + using ObjectStoreFunctions = StoreFunctions>; + /// /// Server session for RESP protocol - basic commands are in this file /// @@ -1663,8 +1668,9 @@ void FlushDb(RespCommand cmd) void ExecuteFlushDb(bool unsafeTruncateLog) { - storeWrapper.store.Log.ShiftBeginAddress(storeWrapper.store.Log.TailAddress, truncateLog: unsafeTruncateLog); - storeWrapper.objectStore?.Log.ShiftBeginAddress(storeWrapper.objectStore.Log.TailAddress, truncateLog: unsafeTruncateLog); + storeWrapper.GetDatabaseStores(activeDbId, out var mainStore, out var objStore); + mainStore.Log.ShiftBeginAddress(mainStore.Log.TailAddress, truncateLog: unsafeTruncateLog); + objStore?.Log.ShiftBeginAddress(objStore.Log.TailAddress, truncateLog: unsafeTruncateLog); } /// diff --git a/libs/server/Resp/GarnetDatabaseSession.cs b/libs/server/Resp/GarnetDatabaseSession.cs new file mode 100644 index 0000000000..3a1bc4a0fc --- /dev/null +++ b/libs/server/Resp/GarnetDatabaseSession.cs @@ -0,0 +1,55 @@ +using System; +using Tsavorite.core; + +namespace Garnet.server +{ + using BasicGarnetApi = GarnetApi, + SpanByteAllocator>>, + BasicContext>, + GenericAllocator>>>>; + using LockableGarnetApi = GarnetApi, + SpanByteAllocator>>, + LockableContext>, + GenericAllocator>>>>; + + internal struct GarnetDatabaseSession : IDisposable + { + /// + /// Storage session + /// + public StorageSession StorageSession; + + /// + /// Garnet API + /// + public BasicGarnetApi GarnetApi; + + /// + /// Lockable Garnet API + /// + public LockableGarnetApi LockableGarnetApi; + + bool disposed = false; + + public GarnetDatabaseSession(StorageSession storageSession, BasicGarnetApi garnetApi, LockableGarnetApi lockableGarnetApi) + { + this.StorageSession = storageSession; + this.GarnetApi = garnetApi; + this.LockableGarnetApi = lockableGarnetApi; + } + + public void Dispose() + { + if (disposed) + return; + + StorageSession?.Dispose(); + + disposed = true; + } + } +} diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index 4a6a8e5b6c..9918b2f860 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -29,7 +29,7 @@ namespace Garnet.server LockableContext>, GenericAllocator>>>>; - + /// /// RESP server session /// @@ -90,12 +90,17 @@ internal sealed unsafe partial class RespServerSession : ServerSessionBase bool toDispose; int opCount; - public readonly StorageSession storageSession; + public StorageSession storageSession; internal BasicGarnetApi basicGarnetApi; internal LockableGarnetApi lockableGarnetApi; readonly IGarnetAuthenticator _authenticator; + readonly bool allowMultiDb; + readonly int maxDbs; + int activeDbId; + ExpandableMap databaseSessions; + /// /// The user currently authenticated in this session /// @@ -207,11 +212,19 @@ public RespServerSession( // Initialize session-local scratch buffer of size 64 bytes, used for constructing arguments in GarnetApi this.scratchBufferManager = new ScratchBufferManager(); - // Create storage session and API - this.storageSession = new StorageSession(storeWrapper, scratchBufferManager, sessionMetrics, LatencyMetrics, logger); + var dbSession = CreateDatabaseSession(0); + maxDbs = storeWrapper.serverOptions.MaxDatabases; + activeDbId = 0; + allowMultiDb = maxDbs > 1; + + if (allowMultiDb) + { + databaseSessions = new ExpandableMap(1, 0, maxDbs - 1); + if (!databaseSessions.TrySetValue(0, ref dbSession)) + throw new GarnetException("Failed to set initial database in databases map"); + } - this.basicGarnetApi = new BasicGarnetApi(storageSession, storageSession.basicContext, storageSession.objectStoreBasicContext); - this.lockableGarnetApi = new LockableGarnetApi(storageSession, storageSession.lockableContext, storageSession.objectStoreLockableContext); + SwitchActiveDatabaseSession(0, ref dbSession); this.storeWrapper = storeWrapper; this.subscribeBroker = subscribeBroker; @@ -243,6 +256,14 @@ public RespServerSession( } } + private GarnetDatabaseSession CreateDatabaseSession(int dbId) + { + var dbStorageSession = new StorageSession(storeWrapper, scratchBufferManager, sessionMetrics, LatencyMetrics, logger, dbId); + var dbGarnetApi = new BasicGarnetApi(dbStorageSession, dbStorageSession.basicContext, dbStorageSession.objectStoreBasicContext); + var dbLockableGarnetApi = new LockableGarnetApi(dbStorageSession, dbStorageSession.lockableContext, dbStorageSession.objectStoreLockableContext); + return new GarnetDatabaseSession(dbStorageSession, dbGarnetApi, dbLockableGarnetApi); + } + internal void SetUser(User user) { this._user = user; @@ -258,6 +279,10 @@ public override void Dispose() try { if (recvHandle.IsAllocated) recvHandle.Free(); } catch { } } + // Dispose all database sessions + foreach (var dbSession in databaseSessions.Map) + dbSession.Dispose(); + if (storeWrapper.serverOptions.MetricsSamplingFrequency > 0 || storeWrapper.serverOptions.LatencyMonitor) storeWrapper.monitor.AddMetricsHistorySessionDispose(sessionMetrics, latencyMetrics); @@ -1215,5 +1240,25 @@ private unsafe ObjectOutputHeader ProcessOutputWithHeader(SpanByteAndMemory outp return header; } + + private void SwitchActiveDatabaseSession(int dbId) + { + if (!allowMultiDb) return; + + if (!databaseSessions.TryGetOrSet(dbId, () => CreateDatabaseSession(dbId), out var db, out _)) + { + throw new GarnetException($"Unable retrieve or add database session with ID {dbId} to the databaseSessions map"); + } + + SwitchActiveDatabaseSession(dbId, ref db); + } + + private void SwitchActiveDatabaseSession(int dbId, ref GarnetDatabaseSession dbSession) + { + this.activeDbId = dbId; + this.storageSession = dbSession.StorageSession; + this.basicGarnetApi = dbSession.GarnetApi; + this.lockableGarnetApi = dbSession.LockableGarnetApi; + } } } \ No newline at end of file diff --git a/libs/server/ServerConfig.cs b/libs/server/ServerConfig.cs index 634114bb31..86107379f8 100644 --- a/libs/server/ServerConfig.cs +++ b/libs/server/ServerConfig.cs @@ -89,7 +89,7 @@ private bool NetworkCONFIG_GET() ReadOnlySpan GetDatabases() { - var databases = storeWrapper.databaseNum.ToString(); + var databases = storeWrapper.databaseCount.ToString(); return Encoding.ASCII.GetBytes($"$9\r\ndatabases\r\n${databases.Length}\r\n{databases}\r\n"); } diff --git a/libs/server/Servers/GarnetServerOptions.cs b/libs/server/Servers/GarnetServerOptions.cs index ecefa235a5..12277a2225 100644 --- a/libs/server/Servers/GarnetServerOptions.cs +++ b/libs/server/Servers/GarnetServerOptions.cs @@ -411,6 +411,11 @@ public class GarnetServerOptions : ServerOptions public LuaOptions LuaOptions; + /// + /// Max number of logical databases allowed + /// + public int MaxDatabases = 16; + /// /// Constructor /// @@ -695,14 +700,15 @@ public KVSettings GetObjectStoreSettings(ILogger logger, /// /// Get AOF settings /// - /// - public void GetAofSettings(out TsavoriteLogSettings tsavoriteLogSettings) + /// DB ID + /// Tsavorite log settings + public void GetAofSettings(int dbId, out TsavoriteLogSettings tsavoriteLogSettings) { tsavoriteLogSettings = new TsavoriteLogSettings { MemorySizeBits = AofMemorySizeBits(), PageSizeBits = AofPageSizeBits(), - LogDevice = GetAofDevice(), + LogDevice = GetAofDevice(dbId), TryRecoverLatest = false, SafeTailRefreshFrequencyMs = EnableCluster ? AofReplicationRefreshFrequencyMs : -1, FastCommitMode = EnableFastCommit, @@ -714,9 +720,10 @@ public void GetAofSettings(out TsavoriteLogSettings tsavoriteLogSettings) logger?.LogError("AOF Page size cannot be more than the AOF memory size."); throw new Exception("AOF Page size cannot be more than the AOF memory size."); } + tsavoriteLogSettings.LogCommitManager = new DeviceLogCommitCheckpointManager( MainMemoryReplication ? new NullNamedDeviceFactory() : DeviceFactoryCreator(), - new DefaultCheckpointNamingScheme(CheckpointDir + "/AOF"), + new DefaultCheckpointNamingScheme(Path.Combine(CheckpointDir, $"AOF{(dbId == 0 ? string.Empty : $"_{dbId}")}")), removeOutdated: true, fastCommitThrottleFreq: EnableFastCommit ? FastCommitThrottleFreq : 0); } @@ -802,12 +809,14 @@ public int ObjectStoreSegmentSizeBits() /// Get device for AOF /// /// - IDevice GetAofDevice() + IDevice GetAofDevice(int dbId) { if (UseAofNullDevice && EnableCluster && !MainMemoryReplication) throw new Exception("Cannot use null device for AOF when cluster is enabled and you are not using main memory replication"); if (UseAofNullDevice) return new NullDevice(); - else return GetInitializedDeviceFactory(CheckpointDir).Get(new FileDescriptor("AOF", "aof.log")); + + return GetInitializedDeviceFactory(CheckpointDir) + .Get(new FileDescriptor($"AOF{(dbId == 0 ? string.Empty : $"_{dbId}")}", "aof.log")); } /// diff --git a/libs/server/Servers/StoreApi.cs b/libs/server/Servers/StoreApi.cs index f9d67cc366..c5b527e849 100644 --- a/libs/server/Servers/StoreApi.cs +++ b/libs/server/Servers/StoreApi.cs @@ -48,10 +48,11 @@ public StoreApi(StoreWrapper storeWrapper) /// Optionally truncate log on disk. This is a destructive operation. Instead take a checkpoint if you are using checkpointing, as /// that will safely truncate the log on disk after the checkpoint. /// - public void FlushDB(bool unsafeTruncateLog = false) + public void FlushDB(int dbId = 0, bool unsafeTruncateLog = false) { - storeWrapper.store.Log.ShiftBeginAddress(storeWrapper.store.Log.TailAddress, truncateLog: unsafeTruncateLog); - storeWrapper.objectStore?.Log.ShiftBeginAddress(storeWrapper.objectStore.Log.TailAddress, truncateLog: unsafeTruncateLog); + storeWrapper.GetDatabaseStores(dbId, out var mainStore, out var objStore); + mainStore.Log.ShiftBeginAddress(mainStore.Log.TailAddress, truncateLog: unsafeTruncateLog); + objStore?.Log.ShiftBeginAddress(objStore.Log.TailAddress, truncateLog: unsafeTruncateLog); } } } \ No newline at end of file diff --git a/libs/server/Storage/Session/StorageSession.cs b/libs/server/Storage/Session/StorageSession.cs index 989dd89536..e157b6cd76 100644 --- a/libs/server/Storage/Session/StorageSession.cs +++ b/libs/server/Storage/Session/StorageSession.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System; +using System.Diagnostics; using Microsoft.Extensions.Logging; using Tsavorite.core; @@ -57,7 +58,8 @@ public StorageSession(StoreWrapper storeWrapper, ScratchBufferManager scratchBufferManager, GarnetSessionMetrics sessionMetrics, GarnetLatencyMetricsSession LatencyMetrics, - ILogger logger = null) + ILogger logger = null, + int dbId = 0) { this.sessionMetrics = sessionMetrics; this.LatencyMetrics = LatencyMetrics; @@ -67,13 +69,15 @@ public StorageSession(StoreWrapper storeWrapper, parseState.Initialize(); - functionsState = storeWrapper.CreateFunctionsState(); + functionsState = storeWrapper.CreateFunctionsState(dbId); var functions = new MainSessionFunctions(functionsState); - var session = storeWrapper.store.NewSession(functions); - var objstorefunctions = new ObjectSessionFunctions(functionsState); - var objectStoreSession = storeWrapper.objectStore?.NewSession(objstorefunctions); + storeWrapper.GetDatabaseStores(dbId, out var mainStore, out var objStore); + var session = mainStore.NewSession(functions); + + var objectStoreFunctions = new ObjectSessionFunctions(functionsState); + var objectStoreSession = objStore?.NewSession(objectStoreFunctions); basicContext = session.BasicContext; lockableContext = session.LockableContext; @@ -83,7 +87,7 @@ public StorageSession(StoreWrapper storeWrapper, objectStoreLockableContext = objectStoreSession.LockableContext; } - HeadAddress = storeWrapper.store.Log.HeadAddress; + HeadAddress = mainStore.Log.HeadAddress; ObjectScanCountLimit = storeWrapper.serverOptions.ObjectScanCountLimit; } diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 6da4f7fca5..1c0cf7ac75 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Net; using System.Net.Sockets; +using System.Text; using System.Threading; using System.Threading.Tasks; using Garnet.common; @@ -36,12 +37,12 @@ public sealed class StoreWrapper /// /// Store /// - public readonly TsavoriteKV store; + public TsavoriteKV store; /// /// Object store /// - public readonly TsavoriteKV objectStore; + public TsavoriteKV objectStore; /// /// Server options @@ -62,7 +63,7 @@ public sealed class StoreWrapper /// /// AOF /// - public readonly TsavoriteLog appendOnlyFile; + public TsavoriteLog appendOnlyFile; /// /// Last save time @@ -79,9 +80,9 @@ public sealed class StoreWrapper internal readonly CollectionItemBroker itemBroker; internal readonly CustomCommandManager customCommandManager; internal readonly GarnetServerMonitor monitor; - internal readonly WatchVersionMap versionMap; + internal WatchVersionMap versionMap; - internal readonly CacheSizeTracker objectStoreSizeTracker; + internal CacheSizeTracker objectStoreSizeTracker; public readonly GarnetObjectSerializer GarnetObjectSerializer; @@ -107,9 +108,17 @@ public sealed class StoreWrapper public readonly TimeSpan loggingFrequncy; /// - /// NOTE: For now we support only a single database + /// Number of current logical databases /// - public readonly int databaseNum = 1; + public int databaseCount = 1; + + /// + /// Delegate for creating a new logical database + /// + internal readonly Func createDatabasesDelegate; + + readonly bool allowMultiDb; + internal ExpandableMap databases; /// /// Constructor @@ -118,38 +127,65 @@ public StoreWrapper( string version, string redisProtocolVersion, IGarnetServer server, - TsavoriteKV store, - TsavoriteKV objectStore, - CacheSizeTracker objectStoreSizeTracker, + Func createsDatabaseDelegate, CustomCommandManager customCommandManager, - TsavoriteLog appendOnlyFile, GarnetServerOptions serverOptions, AccessControlList accessControlList = null, IClusterFactory clusterFactory = null, - ILoggerFactory loggerFactory = null - ) + ILoggerFactory loggerFactory = null) { this.version = version; this.redisProtocolVersion = redisProtocolVersion; this.server = server; this.startupTime = DateTimeOffset.UtcNow.Ticks; - this.store = store; - this.objectStore = objectStore; - this.appendOnlyFile = appendOnlyFile; + this.createDatabasesDelegate = createsDatabaseDelegate; this.serverOptions = serverOptions; lastSaveTime = DateTimeOffset.FromUnixTimeSeconds(0); this.customCommandManager = customCommandManager; - this.monitor = serverOptions.MetricsSamplingFrequency > 0 ? new GarnetServerMonitor(this, serverOptions, server, loggerFactory?.CreateLogger("GarnetServerMonitor")) : null; - this.objectStoreSizeTracker = objectStoreSizeTracker; + this.monitor = serverOptions.MetricsSamplingFrequency > 0 + ? new GarnetServerMonitor(this, serverOptions, server, + loggerFactory?.CreateLogger("GarnetServerMonitor")) + : null; this.loggerFactory = loggerFactory; this.logger = loggerFactory?.CreateLogger("StoreWrapper"); this.sessionLogger = loggerFactory?.CreateLogger("Session"); - // TODO Change map size to a reasonable number - this.versionMap = new WatchVersionMap(1 << 16); this.accessControlList = accessControlList; this.GarnetObjectSerializer = new GarnetObjectSerializer(this.customCommandManager); this.loggingFrequncy = TimeSpan.FromSeconds(serverOptions.LoggingFrequency); + // Create initial stores for default database of index 0 + var db = createDatabasesDelegate(0); + + this.allowMultiDb = this.serverOptions.MaxDatabases > 1; + + // If multiple databases are allowed, create databases map and set initial database + if (this.allowMultiDb) + { + databases = new ExpandableMap(1, 0, this.serverOptions.MaxDatabases - 1); + if (!databases.TrySetValue(0, ref db)) + throw new GarnetException("Failed to set initial database in databases map"); + } + + // Set fields to default database + this.store = db.MainStore; + this.objectStore = db.ObjectStore; + this.objectStoreSizeTracker = db.ObjectSizeTracker; + this.appendOnlyFile = db.AppendOnlyFile; + this.versionMap = db.VersionMap; + + if (logger != null) + { + var configMemoryLimit = (store.IndexSize * 64) + store.Log.MaxMemorySizeBytes + + (store.ReadCache?.MaxMemorySizeBytes ?? 0) + + (appendOnlyFile?.MaxMemorySizeBytes ?? 0); + if (objectStore != null) + configMemoryLimit += (objectStore.IndexSize * 64) + objectStore.Log.MaxMemorySizeBytes + + (objectStore.ReadCache?.MaxMemorySizeBytes ?? 0) + + (objectStoreSizeTracker?.TargetSize ?? 0) + + (objectStoreSizeTracker?.ReadCacheTargetSize ?? 0); + logger.LogInformation("Total configured memory limit: {configMemoryLimit}", configMemoryLimit); + } + if (!serverOptions.DisableObjects) this.itemBroker = new CollectionItemBroker(); @@ -162,15 +198,19 @@ public StoreWrapper( // If ACL authentication is enabled, initiate access control list // NOTE: This is a temporary workflow. ACL should always be initiated and authenticator // should become a parameter of AccessControlList. - if ((this.serverOptions.AuthSettings != null) && (this.serverOptions.AuthSettings.GetType().BaseType == typeof(AclAuthenticationSettings))) + if ((this.serverOptions.AuthSettings != null) && (this.serverOptions.AuthSettings.GetType().BaseType == + typeof(AclAuthenticationSettings))) { // Create a new access control list and register it with the authentication settings - AclAuthenticationSettings aclAuthenticationSettings = (AclAuthenticationSettings)this.serverOptions.AuthSettings; + AclAuthenticationSettings aclAuthenticationSettings = + (AclAuthenticationSettings)this.serverOptions.AuthSettings; if (!string.IsNullOrEmpty(aclAuthenticationSettings.AclConfigurationFile)) { - logger?.LogInformation("Reading ACL configuration file '{filepath}'", aclAuthenticationSettings.AclConfigurationFile); - this.accessControlList = new AccessControlList(aclAuthenticationSettings.DefaultPassword, aclAuthenticationSettings.AclConfigurationFile); + logger?.LogInformation("Reading ACL configuration file '{filepath}'", + aclAuthenticationSettings.AclConfigurationFile); + this.accessControlList = new AccessControlList(aclAuthenticationSettings.DefaultPassword, + aclAuthenticationSettings.AclConfigurationFile); } else { @@ -218,8 +258,19 @@ public string GetIp() return localEndpoint.Address.ToString(); } - internal FunctionsState CreateFunctionsState() - => new(appendOnlyFile, versionMap, customCommandManager, null, objectStoreSizeTracker, GarnetObjectSerializer); + internal FunctionsState CreateFunctionsState(int dbId = 0) + { + Debug.Assert(dbId == 0 || allowMultiDb); + + if (dbId == 0) + return new(appendOnlyFile, versionMap, customCommandManager, null, objectStoreSizeTracker, GarnetObjectSerializer); + + if (!databases.TryGetValue(dbId, out var db)) + throw new GarnetException($"Database with ID {dbId} was not found."); + + return new(db.AppendOnlyFile, db.VersionMap, customCommandManager, null, db.ObjectSizeTracker, + GarnetObjectSerializer); + } internal void Recover() { @@ -687,9 +738,14 @@ private bool GrowIndexIfNeeded(StoreType storeType, long indexMaxSize, long over /// public void Dispose() { - //Wait for checkpoints to complete and disable checkpointing + // Wait for checkpoints to complete and disable checkpointing _checkpointTaskLock.WriteLock(); + // Disable changes to databases map and dispose all databases + databases.mapLock.WriteLock(); + foreach (var db in databases.Map) + db.Dispose(); + itemBroker?.Dispose(); monitor?.Dispose(); ctsCommit?.Cancel(); @@ -898,5 +954,36 @@ public bool HasKeysInSlots(List slots) return false; } + + public void GetDatabaseStores(int dbId, + out TsavoriteKV mainStore, + out TsavoriteKV objStore) + { + if (dbId == 0) + { + mainStore = this.store; + objStore = this.objectStore; + } + else + { + var dbFound = this.TryGetOrSetDatabase(dbId, out var db); + Debug.Assert(dbFound); + mainStore = db.MainStore; + objStore = db.ObjectStore; + } + } + + public bool TryGetOrSetDatabase(int dbId, out GarnetDatabase db) + { + db = default; + + if (!allowMultiDb || !databases.TryGetOrSet(dbId, () => createDatabasesDelegate(dbId), out db, out var added)) + return false; + + if (added) + Interlocked.Increment(ref databaseCount); + + return true; + } } } \ No newline at end of file From b1f44aa4b4bd7cd272db713fc7bced8adbbfe530 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Wed, 29 Jan 2025 13:21:36 -0700 Subject: [PATCH 03/82] wip --- libs/server/AOF/AofProcessor.cs | 2 +- libs/server/Resp/ArrayCommands.cs | 15 ++- libs/server/Resp/CmdStrings.cs | 1 + libs/server/Resp/RespServerSession.cs | 24 ++--- libs/server/ServerConfig.cs | 2 +- libs/server/StoreWrapper.cs | 2 +- test/Garnet.test/MultiDatabaseTests.cs | 134 +++++++++++++++++++++++++ 7 files changed, 161 insertions(+), 19 deletions(-) create mode 100644 test/Garnet.test/MultiDatabaseTests.cs diff --git a/libs/server/AOF/AofProcessor.cs b/libs/server/AOF/AofProcessor.cs index b7e1db75db..c855b95650 100644 --- a/libs/server/AOF/AofProcessor.cs +++ b/libs/server/AOF/AofProcessor.cs @@ -74,7 +74,7 @@ public AofProcessor( null, storeWrapper.createDatabasesDelegate, storeWrapper.customCommandManager, - recordToAof ? storeWrapper.appendOnlyFile : null, + //recordToAof ? storeWrapper.appendOnlyFile : null, storeWrapper.serverOptions, accessControlList: storeWrapper.accessControlList, loggerFactory: storeWrapper.loggerFactory); diff --git a/libs/server/Resp/ArrayCommands.cs b/libs/server/Resp/ArrayCommands.cs index 0291925f1a..239a86294c 100644 --- a/libs/server/Resp/ArrayCommands.cs +++ b/libs/server/Resp/ArrayCommands.cs @@ -235,10 +235,18 @@ private bool NetworkSELECT() return true; } - if (index < storeWrapper.databaseCount) + if (index < storeWrapper.serverOptions.MaxDatabases) { - while (!RespWriteUtils.TryWriteDirect(CmdStrings.RESP_OK, ref dcurr, dend)) - SendAndReset(); + if (index == this.activeDbId || this.TrySwitchActiveDatabaseSession(index)) + { + while (!RespWriteUtils.TryWriteDirect(CmdStrings.RESP_OK, ref dcurr, dend)) + SendAndReset(); + } + else + { + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_DB_INDEX_OUT_OF_RANGE, ref dcurr, dend)) + SendAndReset(); + } } else { @@ -254,6 +262,7 @@ private bool NetworkSELECT() SendAndReset(); } } + return true; } diff --git a/libs/server/Resp/CmdStrings.cs b/libs/server/Resp/CmdStrings.cs index 653b508dfb..92dde15de6 100644 --- a/libs/server/Resp/CmdStrings.cs +++ b/libs/server/Resp/CmdStrings.cs @@ -207,6 +207,7 @@ static partial class CmdStrings public static ReadOnlySpan RESP_ERR_GENERIC_UKNOWN_SUBCOMMAND => "ERR Unknown subcommand. Try LATENCY HELP."u8; public static ReadOnlySpan RESP_ERR_GENERIC_INDEX_OUT_RANGE => "ERR index out of range"u8; public static ReadOnlySpan RESP_ERR_GENERIC_SELECT_INVALID_INDEX => "ERR invalid database index."u8; + public static ReadOnlySpan RESP_ERR_DB_INDEX_OUT_OF_RANGE => "ERR DB index is out of range."u8; public static ReadOnlySpan RESP_ERR_GENERIC_SELECT_CLUSTER_MODE => "ERR SELECT is not allowed in cluster mode"u8; public static ReadOnlySpan RESP_ERR_NO_TRANSACTION_PROCEDURE => "ERR Could not get transaction procedure"u8; public static ReadOnlySpan RESP_ERR_WRONG_NUMBER_OF_ARGUMENTS => "ERR wrong number of arguments for command"u8; diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index 9918b2f860..22cf58634f 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -212,6 +212,13 @@ public RespServerSession( // Initialize session-local scratch buffer of size 64 bytes, used for constructing arguments in GarnetApi this.scratchBufferManager = new ScratchBufferManager(); + this.storeWrapper = storeWrapper; + this.subscribeBroker = subscribeBroker; + this._authenticator = authenticator ?? storeWrapper.serverOptions.AuthSettings?.CreateAuthenticator(this.storeWrapper) ?? new GarnetNoAuthAuthenticator(); + + if (storeWrapper.serverOptions.EnableLua && enableScripts) + sessionScriptCache = new(storeWrapper, _authenticator, logger); + var dbSession = CreateDatabaseSession(0); maxDbs = storeWrapper.serverOptions.MaxDatabases; activeDbId = 0; @@ -226,13 +233,6 @@ public RespServerSession( SwitchActiveDatabaseSession(0, ref dbSession); - this.storeWrapper = storeWrapper; - this.subscribeBroker = subscribeBroker; - this._authenticator = authenticator ?? storeWrapper.serverOptions.AuthSettings?.CreateAuthenticator(this.storeWrapper) ?? new GarnetNoAuthAuthenticator(); - - if (storeWrapper.serverOptions.EnableLua && enableScripts) - sessionScriptCache = new(storeWrapper, _authenticator, logger); - // Associate new session with default user and automatically authenticate, if possible this.AuthenticateUser(Encoding.ASCII.GetBytes(this.storeWrapper.accessControlList.GetDefaultUser().Name)); @@ -1240,17 +1240,15 @@ private unsafe ObjectOutputHeader ProcessOutputWithHeader(SpanByteAndMemory outp return header; } - - private void SwitchActiveDatabaseSession(int dbId) + private bool TrySwitchActiveDatabaseSession(int dbId) { - if (!allowMultiDb) return; + if (!allowMultiDb) return false; if (!databaseSessions.TryGetOrSet(dbId, () => CreateDatabaseSession(dbId), out var db, out _)) - { - throw new GarnetException($"Unable retrieve or add database session with ID {dbId} to the databaseSessions map"); - } + return false; SwitchActiveDatabaseSession(dbId, ref db); + return true; } private void SwitchActiveDatabaseSession(int dbId, ref GarnetDatabaseSession dbSession) diff --git a/libs/server/ServerConfig.cs b/libs/server/ServerConfig.cs index 86107379f8..25a1e77f5d 100644 --- a/libs/server/ServerConfig.cs +++ b/libs/server/ServerConfig.cs @@ -89,7 +89,7 @@ private bool NetworkCONFIG_GET() ReadOnlySpan GetDatabases() { - var databases = storeWrapper.databaseCount.ToString(); + var databases = storeWrapper.serverOptions.MaxDatabases.ToString(); return Encoding.ASCII.GetBytes($"$9\r\ndatabases\r\n${databases.Length}\r\n{databases}\r\n"); } diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 1c0cf7ac75..1d1d587fc4 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -265,7 +265,7 @@ internal FunctionsState CreateFunctionsState(int dbId = 0) if (dbId == 0) return new(appendOnlyFile, versionMap, customCommandManager, null, objectStoreSizeTracker, GarnetObjectSerializer); - if (!databases.TryGetValue(dbId, out var db)) + if (!databases.TryGetOrSet(dbId, () => createDatabasesDelegate(dbId), out var db, out _)) throw new GarnetException($"Database with ID {dbId} was not found."); return new(db.AppendOnlyFile, db.VersionMap, customCommandManager, null, db.ObjectSizeTracker, diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs new file mode 100644 index 0000000000..fbf50e13e8 --- /dev/null +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Azure; +using NUnit.Framework; +using NUnit.Framework.Legacy; +using StackExchange.Redis; + +namespace Garnet.test +{ + [TestFixture] + public class MultiDatabaseTests + { + GarnetServer server; + + [SetUp] + public void Setup() + { + TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir); + server.Start(); + } + + [Test] + public void MultiDatabaseBasicSelectTestSE() + { + var db1Key1 = "db1:key1"; + var db1Key2 = "db1:key2"; + var db2Key1 = "db2:key1"; + var db2Key2 = "db2:key1"; + + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db1 = redis.GetDatabase(0); + + db1.StringSet(db1Key1, "db1:value1"); + db1.ListLeftPush(db1Key2, [new RedisValue("db1:val1"), new RedisValue("db1:val2")]); + + var db2 = redis.GetDatabase(1); + ClassicAssert.IsFalse(db2.KeyExists(db1Key1)); + ClassicAssert.IsFalse(db2.KeyExists(db1Key2)); + + db2.StringSet(db2Key2, "db2:value2"); + db2.SetAdd(db2Key2, [new RedisValue("db2:val2"), new RedisValue("db2:val2")]); + + ClassicAssert.IsFalse(db1.KeyExists(db2Key1)); + ClassicAssert.IsFalse(db1.KeyExists(db2Key2)); + } + + [Test] + public void MultiDatabaseBasicSelectTestLC() + { + var db1Key1 = "db1:key1"; + var db1Key2 = "db1:key2"; + var db2Key1 = "db2:key1"; + var db2Key2 = "db2:key1"; + + using var lightClientRequest = TestUtils.CreateRequest(); + + var response = lightClientRequest.SendCommand($"SET {db1Key1} db1:value1"); + var expectedResponse = "+OK\r\n"; + var actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand($"LPUSH {db1Key2} db1:val1 db1:val2"); + expectedResponse = ":2\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + lightClientRequest.SendCommand($"SELECT 1"); + expectedResponse = "+OK\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand($"EXISTS {db1Key1}"); + expectedResponse = ":0\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand($"EXISTS {db1Key2}"); + expectedResponse = ":0\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand($"SET {db2Key1} db2:value1"); + expectedResponse = "+OK\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand($"SADD {db2Key2} db2:val1 db2:val2"); + expectedResponse = ":2\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + lightClientRequest.SendCommand($"SELECT 0"); + expectedResponse = "+OK\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand($"GET {db1Key1}", 2); + expectedResponse = "$10\r\ndb1:value1\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand($"LPOP {db1Key2}", 2); + expectedResponse = "$8\r\ndb1:val2\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + lightClientRequest.SendCommand($"SELECT 1"); + expectedResponse = "+OK\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand($"GET {db2Key1}", 2); + expectedResponse = "$10\r\ndb2:value1\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand($"SPOP {db2Key2}", 2); + expectedResponse = "$8\r\ndb2:val2\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + } + + [TearDown] + public void TearDown() + { + server.Dispose(); + TestUtils.DeleteDirectory(TestUtils.MethodTestDir); + } + } +} From d2a259f845e10b9ab4d523b52bd178cf0630ecdc Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Wed, 29 Jan 2025 13:26:02 -0700 Subject: [PATCH 04/82] wip --- test/Garnet.test/MultiDatabaseTests.cs | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index fbf50e13e8..1b0033e9da 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Azure; +using System.Text; using NUnit.Framework; using NUnit.Framework.Legacy; using StackExchange.Redis; @@ -48,6 +43,25 @@ public void MultiDatabaseBasicSelectTestSE() ClassicAssert.IsFalse(db1.KeyExists(db2Key2)); } + [Test] + public void MultiDatabaseSameKeyTestSE() + { + var key1 = "key1"; + + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db1 = redis.GetDatabase(0); + db1.StringSet(key1, "db1:value1"); + + var db2 = redis.GetDatabase(1); + db2.SetAdd(key1, [new RedisValue("db2:val2"), new RedisValue("db2:val2")]); + + var db1val = db1.StringGet(key1); + ClassicAssert.AreEqual("db1:value1", db1val.ToString()); + + var db2val = db2.SetPop(key1); + ClassicAssert.AreEqual("db2:val2", db2val.ToString()); + } + [Test] public void MultiDatabaseBasicSelectTestLC() { From 1825fbb08c75dbaeb52688371f525d51bcca6b45 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Wed, 29 Jan 2025 17:08:09 -0700 Subject: [PATCH 05/82] wip --- libs/server/StoreWrapper.cs | 226 +++++++++++++++++++++++++----------- 1 file changed, 157 insertions(+), 69 deletions(-) diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 1d1d587fc4..dc4bca7b2e 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -5,6 +5,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; @@ -394,7 +395,7 @@ public long ReplayAOF(long untilAddress = -1) return replicationOffset; } - async Task AutoCheckpointBasedOnAofSizeLimit(long AofSizeLimit, CancellationToken token = default, ILogger logger = null) + async Task AutoCheckpointBasedOnAofSizeLimit(long aofSizeLimit, CancellationToken token = default, ILogger logger = null) { try { @@ -402,11 +403,27 @@ async Task AutoCheckpointBasedOnAofSizeLimit(long AofSizeLimit, CancellationToke { await Task.Delay(1000); if (token.IsCancellationRequested) break; - var currAofSize = appendOnlyFile.TailAddress - appendOnlyFile.BeginAddress; - if (currAofSize > AofSizeLimit) + var aofSizeAtLimit = -1l; + var databasesMapSize = databases.ActualSize; + var databasesMapSnapshot = databases.Map; + + for (var i = 0; i < databasesMapSize; i++) + { + var db = databasesMapSnapshot[i]; + if (db.Equals(default(GarnetDatabase))) continue; + + var dbAofSize = db.AppendOnlyFile.TailAddress - db.AppendOnlyFile.BeginAddress; + if (dbAofSize > aofSizeLimit) + { + aofSizeAtLimit = dbAofSize; + break; + } + } + + if (aofSizeAtLimit != -1) { - logger?.LogInformation("Enforcing AOF size limit currentAofSize: {currAofSize} > AofSizeLimit: {AofSizeLimit}", currAofSize, AofSizeLimit); + logger?.LogInformation("Enforcing AOF size limit currentAofSize: {currAofSize} > AofSizeLimit: {AofSizeLimit}", aofSizeAtLimit, aofSizeLimit); TakeCheckpoint(false, logger: logger); } } @@ -432,7 +449,17 @@ async Task CommitTask(int commitFrequencyMs, ILogger logger = null, Cancellation } else { - await appendOnlyFile.CommitAsync(null, token); + var databasesMapSize = databases.ActualSize; + var databasesMapSnapshot = databases.Map; + + for (var i = 0; i < databasesMapSize; i++) + { + var db = databasesMapSnapshot[i]; + if (db.Equals(default(GarnetDatabase))) continue; + + await db.AppendOnlyFile.CommitAsync(null, token); + } + await Task.Delay(commitFrequencyMs, token); } } @@ -451,7 +478,18 @@ async Task CompactionTask(int compactionFrequencySecs, CancellationToken token = while (true) { if (token.IsCancellationRequested) return; - DoCompaction(serverOptions.CompactionMaxSegments, serverOptions.ObjectStoreCompactionMaxSegments, 1, serverOptions.CompactionType, serverOptions.CompactionForceDelete); + + var databasesMapSize = databases.ActualSize; + var databasesMapSnapshot = databases.Map; + + for (var i = 0; i < databasesMapSize; i++) + { + var db = databasesMapSnapshot[i]; + if (db.Equals(default(GarnetDatabase))) continue; + + DoCompaction(ref db, serverOptions.CompactionMaxSegments, serverOptions.ObjectStoreCompactionMaxSegments, 1, serverOptions.CompactionType, serverOptions.CompactionForceDelete); + } + if (!serverOptions.CompactionForceDelete) logger?.LogInformation("NOTE: Take a checkpoint (SAVE/BGSAVE) in order to actually delete the older data segments (files) from disk"); else @@ -509,12 +547,12 @@ static void ExecuteHashCollect(ScratchBufferManager scratchBufferManager, Storag } } - void DoCompaction() + void DoCompaction(ref GarnetDatabase db) { // Periodic compaction -> no need to compact before checkpointing if (serverOptions.CompactionFrequencySecs > 0) return; - DoCompaction(serverOptions.CompactionMaxSegments, serverOptions.ObjectStoreCompactionMaxSegments, 1, serverOptions.CompactionType, serverOptions.CompactionForceDelete); + DoCompaction(ref db, serverOptions.CompactionMaxSegments, serverOptions.ObjectStoreCompactionMaxSegments, 1, serverOptions.CompactionType, serverOptions.CompactionForceDelete); } /// @@ -533,15 +571,17 @@ public void EnqueueCommit(bool isMainStore, long version) appendOnlyFile?.Enqueue(header, out _); } - void DoCompaction(int mainStoreMaxSegments, int objectStoreMaxSegments, int numSegmentsToCompact, LogCompactionType compactionType, bool compactionForceDelete) + void DoCompaction(ref GarnetDatabase db, int mainStoreMaxSegments, int objectStoreMaxSegments, int numSegmentsToCompact, LogCompactionType compactionType, bool compactionForceDelete) { if (compactionType == LogCompactionType.None) return; + var mainStoreLog = db.MainStore.Log; + long mainStoreMaxLogSize = (1L << serverOptions.SegmentSizeBits()) * mainStoreMaxSegments; - if (store.Log.ReadOnlyAddress - store.Log.BeginAddress > mainStoreMaxLogSize) + if (mainStoreLog.ReadOnlyAddress - mainStoreLog.BeginAddress > mainStoreMaxLogSize) { - long readOnlyAddress = store.Log.ReadOnlyAddress; + long readOnlyAddress = mainStoreLog.ReadOnlyAddress; long compactLength = (1L << serverOptions.SegmentSizeBits()) * (mainStoreMaxSegments - numSegmentsToCompact); long untilAddress = readOnlyAddress - compactLength; logger?.LogInformation("Begin main store compact until {untilAddress}, Begin = {beginAddress}, ReadOnly = {readOnlyAddress}, Tail = {tailAddress}", untilAddress, store.Log.BeginAddress, readOnlyAddress, store.Log.TailAddress); @@ -549,7 +589,7 @@ void DoCompaction(int mainStoreMaxSegments, int objectStoreMaxSegments, int numS switch (compactionType) { case LogCompactionType.Shift: - store.Log.ShiftBeginAddress(untilAddress, true, compactionForceDelete); + mainStoreLog.ShiftBeginAddress(untilAddress, true, compactionForceDelete); break; case LogCompactionType.Scan: @@ -557,16 +597,16 @@ void DoCompaction(int mainStoreMaxSegments, int objectStoreMaxSegments, int numS if (compactionForceDelete) { CompactionCommitAof(); - store.Log.Truncate(); + mainStoreLog.Truncate(); } break; case LogCompactionType.Lookup: - store.Log.Compact>(new SpanByteFunctions(), untilAddress, CompactionType.Lookup); + mainStoreLog.Compact>(new SpanByteFunctions(), untilAddress, CompactionType.Lookup); if (compactionForceDelete) { CompactionCommitAof(); - store.Log.Truncate(); + mainStoreLog.Truncate(); } break; @@ -577,13 +617,15 @@ void DoCompaction(int mainStoreMaxSegments, int objectStoreMaxSegments, int numS logger?.LogInformation("End main store compact until {untilAddress}, Begin = {beginAddress}, ReadOnly = {readOnlyAddress}, Tail = {tailAddress}", untilAddress, store.Log.BeginAddress, readOnlyAddress, store.Log.TailAddress); } - if (objectStore == null) return; + if (db.ObjectStore == null) return; + + var objectStoreLog = db.ObjectStore.Log; long objectStoreMaxLogSize = (1L << serverOptions.ObjectStoreSegmentSizeBits()) * objectStoreMaxSegments; - if (objectStore.Log.ReadOnlyAddress - objectStore.Log.BeginAddress > objectStoreMaxLogSize) + if (objectStoreLog.ReadOnlyAddress - objectStoreLog.BeginAddress > objectStoreMaxLogSize) { - long readOnlyAddress = objectStore.Log.ReadOnlyAddress; + long readOnlyAddress = objectStoreLog.ReadOnlyAddress; long compactLength = (1L << serverOptions.ObjectStoreSegmentSizeBits()) * (objectStoreMaxSegments - numSegmentsToCompact); long untilAddress = readOnlyAddress - compactLength; logger?.LogInformation("Begin object store compact until {untilAddress}, Begin = {beginAddress}, ReadOnly = {readOnlyAddress}, Tail = {tailAddress}", untilAddress, objectStore.Log.BeginAddress, readOnlyAddress, objectStore.Log.TailAddress); @@ -591,26 +633,26 @@ void DoCompaction(int mainStoreMaxSegments, int objectStoreMaxSegments, int numS switch (compactionType) { case LogCompactionType.Shift: - objectStore.Log.ShiftBeginAddress(untilAddress, compactionForceDelete); + objectStoreLog.ShiftBeginAddress(untilAddress, compactionForceDelete); break; case LogCompactionType.Scan: - objectStore.Log.Compact>( + objectStoreLog.Compact>( new SimpleSessionFunctions(), untilAddress, CompactionType.Scan); if (compactionForceDelete) { CompactionCommitAof(); - objectStore.Log.Truncate(); + objectStoreLog.Truncate(); } break; case LogCompactionType.Lookup: - objectStore.Log.Compact>( + objectStoreLog.Compact>( new SimpleSessionFunctions(), untilAddress, CompactionType.Lookup); if (compactionForceDelete) { CompactionCommitAof(); - objectStore.Log.Truncate(); + objectStoreLog.Truncate(); } break; @@ -682,21 +724,58 @@ private async void IndexAutoGrowTask(CancellationToken token) { try { - bool indexMaxedOut = serverOptions.AdjustedIndexMaxCacheLines == 0; - bool objectStoreIndexMaxedOut = serverOptions.AdjustedObjectStoreIndexMaxCacheLines == 0; - while (!indexMaxedOut || !objectStoreIndexMaxedOut) + var databaseMainStoreIndexMaxedOut = new bool[serverOptions.MaxDatabases]; + var databaseObjectStoreIndexMaxedOut = new bool[serverOptions.MaxDatabases]; + + var databasesMapSize = databases.ActualSize; + var databasesMapSnapshot = databases.Map; + + for (var i = 0; i < databasesMapSize; i++) + { + if (databasesMapSnapshot[i].Equals(default(GarnetDatabase))) continue; + + databaseMainStoreIndexMaxedOut[i] = serverOptions.AdjustedIndexMaxCacheLines == 0; + databaseObjectStoreIndexMaxedOut[i] = serverOptions.AdjustedObjectStoreIndexMaxCacheLines == 0; + } + + var allIndexesMaxedOut = false; + + while (!allIndexesMaxedOut) { + allIndexesMaxedOut = true; + if (token.IsCancellationRequested) break; await Task.Delay(TimeSpan.FromSeconds(serverOptions.IndexResizeFrequencySecs), token); - if (!indexMaxedOut) - indexMaxedOut = GrowIndexIfNeeded(StoreType.Main, serverOptions.AdjustedIndexMaxCacheLines, store.OverflowBucketAllocations, - () => store.IndexSize, () => store.GrowIndex()); + databasesMapSize = databases.ActualSize; + databasesMapSnapshot = databases.Map; - if (!objectStoreIndexMaxedOut) - objectStoreIndexMaxedOut = GrowIndexIfNeeded(StoreType.Object, serverOptions.AdjustedObjectStoreIndexMaxCacheLines, objectStore.OverflowBucketAllocations, - () => objectStore.IndexSize, () => objectStore.GrowIndex()); + for (var i = 0; i < databasesMapSize; i++) + { + if (!databaseMainStoreIndexMaxedOut[i]) + { + var dbMainStore = databasesMapSnapshot[i].MainStore; + databaseMainStoreIndexMaxedOut[i] = GrowIndexIfNeeded(StoreType.Main, + serverOptions.AdjustedIndexMaxCacheLines, dbMainStore.OverflowBucketAllocations, + () => dbMainStore.IndexSize, () => dbMainStore.GrowIndex()); + + if (!databaseMainStoreIndexMaxedOut[i]) + allIndexesMaxedOut = false; + } + + if (!databaseObjectStoreIndexMaxedOut[i]) + { + var dbObjectStore = databasesMapSnapshot[i].ObjectStore; + databaseObjectStoreIndexMaxedOut[i] = GrowIndexIfNeeded(StoreType.Object, + serverOptions.AdjustedObjectStoreIndexMaxCacheLines, + dbObjectStore.OverflowBucketAllocations, + () => dbObjectStore.IndexSize, () => dbObjectStore.GrowIndex()); + + if (!databaseObjectStoreIndexMaxedOut[i]) + allIndexesMaxedOut = false; + } + } } } catch (Exception ex) @@ -814,31 +893,40 @@ private async Task CheckpointTask(StoreType storeType, ILogger logger = null) { try { - DoCompaction(); - var lastSaveStoreTailAddress = store.Log.TailAddress; - var lastSaveObjectStoreTailAddress = (objectStore?.Log.TailAddress).GetValueOrDefault(); - - var full = false; - if (this.lastSaveStoreTailAddress == 0 || lastSaveStoreTailAddress - this.lastSaveStoreTailAddress >= serverOptions.FullCheckpointLogInterval) - full = true; - if (objectStore != null && (this.lastSaveObjectStoreTailAddress == 0 || lastSaveObjectStoreTailAddress - this.lastSaveObjectStoreTailAddress >= serverOptions.FullCheckpointLogInterval)) - full = true; - - var tryIncremental = serverOptions.EnableIncrementalSnapshots; - if (store.IncrementalSnapshotTailAddress >= serverOptions.IncrementalSnapshotLogSizeLimit) - tryIncremental = false; - if (objectStore?.IncrementalSnapshotTailAddress >= serverOptions.IncrementalSnapshotLogSizeLimit) - tryIncremental = false; - - var checkpointType = serverOptions.UseFoldOverCheckpoints ? CheckpointType.FoldOver : CheckpointType.Snapshot; - await InitiateCheckpoint(full, checkpointType, tryIncremental, storeType, logger); - if (full) + var databasesMapSize = databases.ActualSize; + var databasesMapSnapshot = databases.Map; + + for (var i = 0; i < databasesMapSize; i++) { - if (storeType is StoreType.Main or StoreType.All) - this.lastSaveStoreTailAddress = lastSaveStoreTailAddress; - if (storeType is StoreType.Object or StoreType.All) - this.lastSaveObjectStoreTailAddress = lastSaveObjectStoreTailAddress; + var db = databasesMapSnapshot[i]; + if (db.Equals(default(GarnetDatabase))) continue; + + DoCompaction(ref db); + var lastSaveStoreTailAddress = store.Log.TailAddress; + var lastSaveObjectStoreTailAddress = (objectStore?.Log.TailAddress).GetValueOrDefault(); + + var full = this.lastSaveStoreTailAddress == 0 || + lastSaveStoreTailAddress - this.lastSaveStoreTailAddress >= serverOptions.FullCheckpointLogInterval || + (objectStore != null && (this.lastSaveObjectStoreTailAddress == 0 || + lastSaveObjectStoreTailAddress - this.lastSaveObjectStoreTailAddress >= serverOptions.FullCheckpointLogInterval)); + + var tryIncremental = serverOptions.EnableIncrementalSnapshots; + if (store.IncrementalSnapshotTailAddress >= serverOptions.IncrementalSnapshotLogSizeLimit) + tryIncremental = false; + if (objectStore?.IncrementalSnapshotTailAddress >= serverOptions.IncrementalSnapshotLogSizeLimit) + tryIncremental = false; + + var checkpointType = serverOptions.UseFoldOverCheckpoints ? CheckpointType.FoldOver : CheckpointType.Snapshot; + await InitiateCheckpoint(i, db, full, checkpointType, tryIncremental, storeType, logger); + if (full) + { + if (storeType is StoreType.Main or StoreType.All) + this.lastSaveStoreTailAddress = lastSaveStoreTailAddress; + if (storeType is StoreType.Object or StoreType.All) + this.lastSaveObjectStoreTailAddress = lastSaveObjectStoreTailAddress; + } } + lastSaveTime = DateTimeOffset.UtcNow; } catch (Exception ex) @@ -851,17 +939,17 @@ private async Task CheckpointTask(StoreType storeType, ILogger logger = null) } } - private async Task InitiateCheckpoint(bool full, CheckpointType checkpointType, bool tryIncremental, StoreType storeType, ILogger logger = null) + private async Task InitiateCheckpoint(int dbId, GarnetDatabase db, bool full, CheckpointType checkpointType, bool tryIncremental, StoreType storeType, ILogger logger = null) { - logger?.LogInformation("Initiating checkpoint; full = {full}, type = {checkpointType}, tryIncremental = {tryIncremental}, storeType = {storeType}", full, checkpointType, tryIncremental, storeType); + logger?.LogInformation("Initiating checkpoint; full = {full}, type = {checkpointType}, tryIncremental = {tryIncremental}, storeType = {storeType}, dbId = {dbId}", full, checkpointType, tryIncremental, storeType, dbId); long CheckpointCoveredAofAddress = 0; - if (appendOnlyFile != null) + if (db.AppendOnlyFile != null) { if (serverOptions.EnableCluster) clusterProvider.OnCheckpointInitiated(out CheckpointCoveredAofAddress); else - CheckpointCoveredAofAddress = appendOnlyFile.TailAddress; + CheckpointCoveredAofAddress = db.AppendOnlyFile.TailAddress; if (CheckpointCoveredAofAddress > 0) logger?.LogInformation("Will truncate AOF to {tailAddress} after checkpoint (files deleted after next commit)", CheckpointCoveredAofAddress); @@ -872,18 +960,18 @@ private async Task InitiateCheckpoint(bool full, CheckpointType checkpointType, if (full) { if (storeType is StoreType.Main or StoreType.All) - storeCheckpointResult = await store.TakeFullCheckpointAsync(checkpointType); + storeCheckpointResult = await db.MainStore.TakeFullCheckpointAsync(checkpointType); - if (objectStore != null && (storeType == StoreType.Object || storeType == StoreType.All)) - objectStoreCheckpointResult = await objectStore.TakeFullCheckpointAsync(checkpointType); + if (db.ObjectStore != null && (storeType == StoreType.Object || storeType == StoreType.All)) + objectStoreCheckpointResult = await db.ObjectStore.TakeFullCheckpointAsync(checkpointType); } else { if (storeType is StoreType.Main or StoreType.All) - storeCheckpointResult = await store.TakeHybridLogCheckpointAsync(checkpointType, tryIncremental); + storeCheckpointResult = await db.MainStore.TakeHybridLogCheckpointAsync(checkpointType, tryIncremental); - if (objectStore != null && (storeType == StoreType.Object || storeType == StoreType.All)) - objectStoreCheckpointResult = await objectStore.TakeHybridLogCheckpointAsync(checkpointType, tryIncremental); + if (db.ObjectStore != null && (storeType == StoreType.Object || storeType == StoreType.All)) + objectStoreCheckpointResult = await db.ObjectStore.TakeHybridLogCheckpointAsync(checkpointType, tryIncremental); } // If cluster is enabled the replication manager is responsible for truncating AOF @@ -893,15 +981,15 @@ private async Task InitiateCheckpoint(bool full, CheckpointType checkpointType, } else { - appendOnlyFile?.TruncateUntil(CheckpointCoveredAofAddress); - appendOnlyFile?.Commit(); + db.AppendOnlyFile?.TruncateUntil(CheckpointCoveredAofAddress); + db.AppendOnlyFile?.Commit(); } - if (objectStore != null) + if (db.ObjectStore != null) { // During the checkpoint, we may have serialized Garnet objects in (v) versions of objects. // We can now safely remove these serialized versions as they are no longer needed. - using (var iter1 = objectStore.Log.Scan(objectStore.Log.ReadOnlyAddress, objectStore.Log.TailAddress, ScanBufferingMode.SinglePageBuffering, includeSealedRecords: true)) + using (var iter1 = db.ObjectStore.Log.Scan(db.ObjectStore.Log.ReadOnlyAddress, db.ObjectStore.Log.TailAddress, ScanBufferingMode.SinglePageBuffering, includeSealedRecords: true)) { while (iter1.GetNext(out _, out _, out var value)) { From c4d98b440405033cc68e836fd4e3c65c1d1cb42c Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 30 Jan 2025 11:15:08 -0700 Subject: [PATCH 06/82] wip --- libs/server/GarnetDatabase.cs | 12 +++ libs/server/StoreWrapper.cs | 87 +++++++++++++-------- test/Garnet.test/MultiDatabaseTests.cs | 100 ++++++++++++++++++++++++- 3 files changed, 168 insertions(+), 31 deletions(-) diff --git a/libs/server/GarnetDatabase.cs b/libs/server/GarnetDatabase.cs index 36ee67cbab..a3166086f4 100644 --- a/libs/server/GarnetDatabase.cs +++ b/libs/server/GarnetDatabase.cs @@ -50,6 +50,16 @@ public struct GarnetDatabase : IDisposable /// public WatchVersionMap VersionMap; + /// + /// Tail address of main store log at last save + /// + public long LastSaveStoreTailAddress; + + /// + /// Tail address of object store log at last save + /// + public long LastSaveObjectStoreTailAddress; + bool disposed = false; public GarnetDatabase(TsavoriteKV mainStore, @@ -62,6 +72,8 @@ public GarnetDatabase(TsavoriteKV public DateTimeOffset lastSaveTime; - internal long lastSaveStoreTailAddress; - internal long lastSaveObjectStoreTailAddress; /// /// Logger factory @@ -121,6 +119,8 @@ public sealed class StoreWrapper readonly bool allowMultiDb; internal ExpandableMap databases; + string activeDbIdsPath; + /// /// Constructor /// @@ -154,18 +154,18 @@ public StoreWrapper( this.GarnetObjectSerializer = new GarnetObjectSerializer(this.customCommandManager); this.loggingFrequncy = TimeSpan.FromSeconds(serverOptions.LoggingFrequency); + var checkpointDir = serverOptions.CheckpointDir ?? serverOptions.LogDir; + activeDbIdsPath = Path.Combine(checkpointDir, "activeDbIds.dat"); + // Create initial stores for default database of index 0 var db = createDatabasesDelegate(0); this.allowMultiDb = this.serverOptions.MaxDatabases > 1; - // If multiple databases are allowed, create databases map and set initial database - if (this.allowMultiDb) - { - databases = new ExpandableMap(1, 0, this.serverOptions.MaxDatabases - 1); - if (!databases.TrySetValue(0, ref db)) - throw new GarnetException("Failed to set initial database in databases map"); - } + // Create databases map and set initial database + databases = new ExpandableMap(1, 0, this.serverOptions.MaxDatabases - 1); + if (!databases.TrySetValue(0, ref db)) + throw new GarnetException("Failed to set initial database in databases map"); // Set fields to default database this.store = db.MainStore; @@ -318,8 +318,26 @@ public void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStore } else { - storeVersion = store.Recover(); - if (objectStore != null) objectStoreVersion = objectStore.Recover(); + using (var reader = new BinaryReader(File.Open(activeDbIdsPath, FileMode.Open))) + { + while (reader.BaseStream.Position < reader.BaseStream.Length) + { + var dbId = reader.ReadInt32(); + if (dbId == 0) continue; + + var db = createDatabasesDelegate(dbId); + databases.TrySetValue(dbId, ref db); + } + } + + var databasesMapSize = databases.ActualSize; + var databasesMapSnapshot = databases.Map; + for (var i = 0; i < databasesMapSize; i++) + { + var db = databasesMapSnapshot[i]; + storeVersion = db.MainStore.Recover(); + if (db.ObjectStore != null) objectStoreVersion = db.ObjectStore.Recover(); + } } if (storeVersion > 0 || objectStoreVersion > 0) lastSaveTime = DateTimeOffset.UtcNow; @@ -584,7 +602,7 @@ void DoCompaction(ref GarnetDatabase db, int mainStoreMaxSegments, int objectSto long readOnlyAddress = mainStoreLog.ReadOnlyAddress; long compactLength = (1L << serverOptions.SegmentSizeBits()) * (mainStoreMaxSegments - numSegmentsToCompact); long untilAddress = readOnlyAddress - compactLength; - logger?.LogInformation("Begin main store compact until {untilAddress}, Begin = {beginAddress}, ReadOnly = {readOnlyAddress}, Tail = {tailAddress}", untilAddress, store.Log.BeginAddress, readOnlyAddress, store.Log.TailAddress); + logger?.LogInformation("Begin main store compact until {untilAddress}, Begin = {beginAddress}, ReadOnly = {readOnlyAddress}, Tail = {tailAddress}", untilAddress, mainStoreLog.BeginAddress, readOnlyAddress, mainStoreLog.TailAddress); switch (compactionType) { @@ -593,7 +611,7 @@ void DoCompaction(ref GarnetDatabase db, int mainStoreMaxSegments, int objectSto break; case LogCompactionType.Scan: - store.Log.Compact>(new SpanByteFunctions(), untilAddress, CompactionType.Scan); + mainStoreLog.Compact>(new SpanByteFunctions(), untilAddress, CompactionType.Scan); if (compactionForceDelete) { CompactionCommitAof(); @@ -614,7 +632,7 @@ void DoCompaction(ref GarnetDatabase db, int mainStoreMaxSegments, int objectSto break; } - logger?.LogInformation("End main store compact until {untilAddress}, Begin = {beginAddress}, ReadOnly = {readOnlyAddress}, Tail = {tailAddress}", untilAddress, store.Log.BeginAddress, readOnlyAddress, store.Log.TailAddress); + logger?.LogInformation("End main store compact until {untilAddress}, Begin = {beginAddress}, ReadOnly = {readOnlyAddress}, Tail = {tailAddress}", untilAddress, mainStoreLog.BeginAddress, readOnlyAddress, mainStoreLog.TailAddress); } if (db.ObjectStore == null) return; @@ -660,7 +678,7 @@ void DoCompaction(ref GarnetDatabase db, int mainStoreMaxSegments, int objectSto break; } - logger?.LogInformation("End object store compact until {untilAddress}, Begin = {beginAddress}, ReadOnly = {readOnlyAddress}, Tail = {tailAddress}", untilAddress, store.Log.BeginAddress, readOnlyAddress, store.Log.TailAddress); + logger?.LogInformation("End object store compact until {untilAddress}, Begin = {beginAddress}, ReadOnly = {readOnlyAddress}, Tail = {tailAddress}", untilAddress, mainStoreLog.BeginAddress, readOnlyAddress, mainStoreLog.TailAddress); } } @@ -896,37 +914,46 @@ private async Task CheckpointTask(StoreType storeType, ILogger logger = null) var databasesMapSize = databases.ActualSize; var databasesMapSnapshot = databases.Map; - for (var i = 0; i < databasesMapSize; i++) + var activeDbIds = new List(); + for (var dbId = 0; dbId < databasesMapSize; dbId++) { - var db = databasesMapSnapshot[i]; + var db = databasesMapSnapshot[dbId]; if (db.Equals(default(GarnetDatabase))) continue; DoCompaction(ref db); - var lastSaveStoreTailAddress = store.Log.TailAddress; - var lastSaveObjectStoreTailAddress = (objectStore?.Log.TailAddress).GetValueOrDefault(); + var lastSaveStoreTailAddress = db.MainStore.Log.TailAddress; + var lastSaveObjectStoreTailAddress = (db.ObjectStore?.Log.TailAddress).GetValueOrDefault(); - var full = this.lastSaveStoreTailAddress == 0 || - lastSaveStoreTailAddress - this.lastSaveStoreTailAddress >= serverOptions.FullCheckpointLogInterval || - (objectStore != null && (this.lastSaveObjectStoreTailAddress == 0 || - lastSaveObjectStoreTailAddress - this.lastSaveObjectStoreTailAddress >= serverOptions.FullCheckpointLogInterval)); + var full = db.LastSaveStoreTailAddress == 0 || + lastSaveStoreTailAddress - db.LastSaveStoreTailAddress >= serverOptions.FullCheckpointLogInterval || + (db.ObjectStore != null && (db.LastSaveObjectStoreTailAddress == 0 || + lastSaveObjectStoreTailAddress - db.LastSaveObjectStoreTailAddress >= serverOptions.FullCheckpointLogInterval)); var tryIncremental = serverOptions.EnableIncrementalSnapshots; - if (store.IncrementalSnapshotTailAddress >= serverOptions.IncrementalSnapshotLogSizeLimit) + if (db.MainStore.IncrementalSnapshotTailAddress >= serverOptions.IncrementalSnapshotLogSizeLimit) tryIncremental = false; - if (objectStore?.IncrementalSnapshotTailAddress >= serverOptions.IncrementalSnapshotLogSizeLimit) + if (db.ObjectStore?.IncrementalSnapshotTailAddress >= serverOptions.IncrementalSnapshotLogSizeLimit) tryIncremental = false; var checkpointType = serverOptions.UseFoldOverCheckpoints ? CheckpointType.FoldOver : CheckpointType.Snapshot; - await InitiateCheckpoint(i, db, full, checkpointType, tryIncremental, storeType, logger); + await InitiateCheckpoint(dbId, db, full, checkpointType, tryIncremental, storeType, logger); if (full) { if (storeType is StoreType.Main or StoreType.All) - this.lastSaveStoreTailAddress = lastSaveStoreTailAddress; + db.LastSaveStoreTailAddress = lastSaveStoreTailAddress; if (storeType is StoreType.Object or StoreType.All) - this.lastSaveObjectStoreTailAddress = lastSaveObjectStoreTailAddress; + db.LastSaveObjectStoreTailAddress = lastSaveObjectStoreTailAddress; } + + activeDbIds.Add(dbId); } - + + await using (var bw = new BinaryWriter(File.Open(activeDbIdsPath, FileMode.Create))) + { + foreach (var dbIds in activeDbIds) + bw.Write(dbIds); + } + lastSaveTime = DateTimeOffset.UtcNow; } catch (Exception ex) diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index 1b0033e9da..b2022e0165 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -1,4 +1,7 @@ -using System.Text; +using System.Linq; +using System; +using System.Text; +using System.Threading; using NUnit.Framework; using NUnit.Framework.Legacy; using StackExchange.Redis; @@ -138,6 +141,101 @@ public void MultiDatabaseBasicSelectTestLC() ClassicAssert.AreEqual(expectedResponse, actualValue); } + [Test] + public void MultiDatabaseSaveRecoverObjectTest() + { + var db1Key = "db1:key1"; + var db2Key = "db1:key1"; + var db1data = new RedisValue[] { "db1:a", "db1:b", "db1:c", "db1:d" }; + var db2data = new RedisValue[] { "db2:a", "db2:b", "db2:c", "db2:d" }; + RedisValue[] db1DataBeforeRecovery; + RedisValue[] db2DataBeforeRecovery; + + using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig(allowAdmin: true))) + { + var db1 = redis.GetDatabase(0); + db1.ListLeftPush(db1Key, db1data); + db1data = db1data.Select(x => x).Reverse().ToArray(); + db1DataBeforeRecovery = db1.ListRange(db1Key); + ClassicAssert.AreEqual(db1data, db1DataBeforeRecovery); + + var db2 = redis.GetDatabase(1); + db2.SetAdd(db2Key, db2data); + db2DataBeforeRecovery = db2.SetMembers(db2Key); + ClassicAssert.AreEqual(db2data, db2DataBeforeRecovery); + + // Issue and wait for DB save + var garnetServer = redis.GetServer($"{TestUtils.Address}:{TestUtils.Port}"); + garnetServer.Save(SaveType.BackgroundSave); + while (garnetServer.LastSave().Ticks == DateTimeOffset.FromUnixTimeSeconds(0).Ticks) Thread.Sleep(10); + } + + server.Dispose(false); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, tryRecover: true); + server.Start(); + + using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig(allowAdmin: true))) + { + var db1 = redis.GetDatabase(0); + var db1ReturnedData = db1.ListRange(db1Key); + ClassicAssert.AreEqual(db1DataBeforeRecovery, db1ReturnedData); + ClassicAssert.AreEqual(db1data.Length, db1ReturnedData.Length); + ClassicAssert.AreEqual(db1data, db1ReturnedData); + + var db2 = redis.GetDatabase(1); + var db2ReturnedData = db2.SetMembers(db2Key); + ClassicAssert.AreEqual(db2DataBeforeRecovery, db2ReturnedData); + ClassicAssert.AreEqual(db2data.Length, db2ReturnedData.Length); + ClassicAssert.AreEqual(db2data, db2ReturnedData); + } + } + + [Test] + public void MultiDatabaseSaveRecoverRawStringTest() + { + var db1Key = "db1:key1"; + var db2Key = "db1:key1"; + var db1data = new RedisValue("db1:a"); + var db2data = new RedisValue("db2:a"); + RedisValue db1DataBeforeRecovery; + RedisValue db2DataBeforeRecovery; + + using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig(allowAdmin: true))) + { + var db1 = redis.GetDatabase(0); + db1.StringSet(db1Key, db1data); + db1DataBeforeRecovery = db1.StringGet(db1Key); + ClassicAssert.AreEqual(db1data, db1DataBeforeRecovery); + + var db2 = redis.GetDatabase(1); + db2.StringSet(db2Key, db2data); + db2DataBeforeRecovery = db2.StringGet(db2Key); + ClassicAssert.AreEqual(db2data, db2DataBeforeRecovery); + + // Issue and wait for DB save + var garnetServer = redis.GetServer($"{TestUtils.Address}:{TestUtils.Port}"); + garnetServer.Save(SaveType.BackgroundSave); + while (garnetServer.LastSave().Ticks == DateTimeOffset.FromUnixTimeSeconds(0).Ticks) Thread.Sleep(10); + } + + server.Dispose(false); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, tryRecover: true); + server.Start(); + + using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig(allowAdmin: true))) + { + var db1 = redis.GetDatabase(0); + var db1ReturnedData = db1.StringGet(db1Key); + ClassicAssert.AreEqual(db1DataBeforeRecovery, db1ReturnedData); + ClassicAssert.AreEqual(db1data, db1ReturnedData); + + var db2 = redis.GetDatabase(1); + var db2ReturnedData = db2.StringGet(db2Key); + ClassicAssert.AreEqual(db2DataBeforeRecovery, db2ReturnedData); + ClassicAssert.AreEqual(db2data, db2ReturnedData); + } + } + [TearDown] public void TearDown() { From 41bdbd7aa8461e147e9ebd45841df02cddd8500c Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 30 Jan 2025 20:13:58 -0700 Subject: [PATCH 07/82] wip --- libs/host/Configuration/Options.cs | 3 + libs/host/GarnetServer.cs | 35 +++++----- libs/server/Servers/GarnetServerOptions.cs | 25 ++++++- libs/server/StoreWrapper.cs | 77 ++++++++++++++++------ test/Garnet.test/MultiDatabaseTests.cs | 50 +++++++++++++- 5 files changed, 151 insertions(+), 39 deletions(-) diff --git a/libs/host/Configuration/Options.cs b/libs/host/Configuration/Options.cs index a50e35d5c8..e482c40ed3 100644 --- a/libs/host/Configuration/Options.cs +++ b/libs/host/Configuration/Options.cs @@ -11,6 +11,7 @@ using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; using CommandLine; +using Garnet.common; using Garnet.server; using Garnet.server.Auth.Aad; using Garnet.server.Auth.Settings; @@ -716,6 +717,8 @@ public GarnetServerOptions GetServerOptions(ILogger logger = null) DeviceFactoryCreator = useAzureStorage ? () => new AzureStorageNamedDeviceFactory(AzureStorageConnectionString, logger) : () => new LocalStorageNamedDeviceFactory(useNativeDeviceLinux: UseNativeDeviceLinux.GetValueOrDefault(), logger: logger), + StreamProviderCreator = () => StreamProviderFactory.GetStreamProvider( + useAzureStorage ? FileLocationType.AzureStorage : FileLocationType.Local, AzureStorageConnectionString), CheckpointThrottleFlushDelayMs = CheckpointThrottleFlushDelayMs, EnableScatterGatherGet = EnableScatterGatherGet.GetValueOrDefault(), ReplicaSyncDelayMs = ReplicaSyncDelayMs, diff --git a/libs/host/GarnetServer.cs b/libs/host/GarnetServer.cs index c91a5239ea..f09cd71311 100644 --- a/libs/host/GarnetServer.cs +++ b/libs/host/GarnetServer.cs @@ -220,14 +220,8 @@ private void InitializeServer() if (!setMax && !ThreadPool.SetMaxThreads(maxThreads, maxCPThreads)) throw new Exception($"Unable to call ThreadPool.SetMaxThreads with {maxThreads}, {maxCPThreads}"); - var createDatabaseDelegate = (int dbId) => - { - var store = CreateMainStore(dbId, clusterFactory, out var currCheckpointDir); - var objectStore = CreateObjectStore(dbId, clusterFactory, customCommandManager, currCheckpointDir, - out var objectStoreSizeTracker); - var (aofDevice, aof) = CreateAOF(dbId); - return new GarnetDatabase(store, objectStore, objectStoreSizeTracker, aofDevice, aof); - }; + StoreWrapper.DatabaseCreatorDelegate createDatabaseDelegate = (int dbId, out string storeCheckpointDir, out string aofDir) => + CreateDatabase(dbId, clusterFactory, customCommandManager, out storeCheckpointDir, out aofDir); if (!opts.DisablePubSub) subscribeBroker = new SubscribeBroker>(new SpanByteKeySerializer(), null, opts.PubSubPageSizeBytes(), opts.SubscriberRefreshFrequencyMs, true); @@ -238,7 +232,7 @@ private void InitializeServer() this.server ??= new GarnetServerTcp(opts.Address, opts.Port, 0, opts.TlsOptions, opts.NetworkSendThrottleMax, opts.NetworkConnectionLimit, logger); storeWrapper = new StoreWrapper(version, redisProtocolVersion, server, createDatabaseDelegate, - customCommandManager, opts, clusterFactory: clusterFactory, loggerFactory: loggerFactory); + customCommandManager, opts, clusterFactory: clusterFactory, loggerFactory: loggerFactory); // Create session provider for Garnet Provider = new GarnetProvider(storeWrapper, subscribeBroker); @@ -253,6 +247,15 @@ private void InitializeServer() LoadModules(customCommandManager); } + private GarnetDatabase CreateDatabase(int dbId, ClusterFactory clusterFactory, CustomCommandManager customCommandManager, out string storeCheckpointDir, out string aofDir) + { + var store = CreateMainStore(dbId, clusterFactory, out var checkpointDir, out storeCheckpointDir); + var objectStore = CreateObjectStore(dbId, clusterFactory, customCommandManager, checkpointDir, + out var objectStoreSizeTracker); + var (aofDevice, aof) = CreateAOF(dbId, out aofDir); + return new GarnetDatabase(store, objectStore, objectStoreSizeTracker, aofDevice, aof); + } + private void LoadModules(CustomCommandManager customCommandManager) { if (opts.LoadModuleCS == null) @@ -277,7 +280,7 @@ private void LoadModules(CustomCommandManager customCommandManager) } } - private TsavoriteKV CreateMainStore(int dbId, IClusterFactory clusterFactory, out string checkpointDir) + private TsavoriteKV CreateMainStore(int dbId, IClusterFactory clusterFactory, out string checkpointDir, out string mainStoreCheckpointDir) { kvSettings = opts.GetSettings(loggerFactory, out logFactory); @@ -288,9 +291,10 @@ private TsavoriteKV kvSettings.CheckpointVersionSwitchBarrier = opts.EnableCluster; var checkpointFactory = opts.DeviceFactoryCreator(); - var baseName = Path.Combine(checkpointDir, "Store", $"checkpoints{(dbId == 0 ? string.Empty : $"_{dbId}")}"); + mainStoreCheckpointDir = Path.Combine(checkpointDir, "Store"); + var baseName = Path.Combine(mainStoreCheckpointDir, $"checkpoints{(dbId == 0 ? string.Empty : $"_{dbId}")}"); var defaultNamingScheme = new DefaultCheckpointNamingScheme(baseName); - + kvSettings.CheckpointManager = opts.EnableCluster ? clusterFactory.CreateCheckpointManager(checkpointFactory, defaultNamingScheme, isMainStore: true, logger) : new DeviceLogCommitCheckpointManager(checkpointFactory, defaultNamingScheme, removeOutdated: true); @@ -336,17 +340,18 @@ private TsavoriteKV public Func DeviceFactoryCreator = null; + /// + /// Creator of stream providers + /// + public Func StreamProviderCreator = null; + /// /// Whether and by how much should we throttle the disk IO for checkpoints (default = 0) /// -1 - disable throttling @@ -481,6 +487,7 @@ public KVSettings GetSettings(ILoggerFactory loggerFactory, logger?.LogInformation("[Store] Using log mutable percentage of {MutablePercent}%", MutablePercent); DeviceFactoryCreator ??= () => new LocalStorageNamedDeviceFactory(useNativeDeviceLinux: UseNativeDeviceLinux, logger: logger); + StreamProviderCreator ??= () => StreamProviderFactory.GetStreamProvider(FileLocationType.Local); if (LatencyMonitor && MetricsSamplingFrequency == 0) throw new Exception("LatencyMonitor requires MetricsSamplingFrequency to be set"); @@ -702,7 +709,8 @@ public KVSettings GetObjectStoreSettings(ILogger logger, /// /// DB ID /// Tsavorite log settings - public void GetAofSettings(int dbId, out TsavoriteLogSettings tsavoriteLogSettings) + /// + public void GetAofSettings(int dbId, out TsavoriteLogSettings tsavoriteLogSettings, out string aofDir) { tsavoriteLogSettings = new TsavoriteLogSettings { @@ -721,9 +729,10 @@ public void GetAofSettings(int dbId, out TsavoriteLogSettings tsavoriteLogSettin throw new Exception("AOF Page size cannot be more than the AOF memory size."); } + aofDir = Path.Combine(CheckpointDir, $"AOF{(dbId == 0 ? string.Empty : $"_{dbId}")}"); tsavoriteLogSettings.LogCommitManager = new DeviceLogCommitCheckpointManager( MainMemoryReplication ? new NullNamedDeviceFactory() : DeviceFactoryCreator(), - new DefaultCheckpointNamingScheme(Path.Combine(CheckpointDir, $"AOF{(dbId == 0 ? string.Empty : $"_{dbId}")}")), + new DefaultCheckpointNamingScheme(aofDir), removeOutdated: true, fastCommitThrottleFreq: EnableFastCommit ? FastCommitThrottleFreq : 0); } @@ -819,6 +828,18 @@ IDevice GetAofDevice(int dbId) .Get(new FileDescriptor($"AOF{(dbId == 0 ? string.Empty : $"_{dbId}")}", "aof.log")); } + /// + /// Get device for logging database IDs + /// + /// + public IDevice GetDatabaseIdsDevice() + { + if (MaxDatabases == 1) return new NullDevice(); + + return GetInitializedDeviceFactory(CheckpointDir) + .Get(new FileDescriptor($"databases", "ids.dat")); + } + /// /// Get device factory /// diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 6633e0d440..573fed5f44 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -4,18 +4,21 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.ComponentModel.Design; using System.Diagnostics; using System.IO; using System.Net; using System.Net.Sockets; -using System.Text; +using System.Reflection; using System.Threading; using System.Threading.Tasks; +using System.Xml; using Garnet.common; using Garnet.server.ACL; using Garnet.server.Auth.Settings; using Microsoft.Extensions.Logging; using Tsavorite.core; +using IStreamProvider = Garnet.common.IStreamProvider; namespace Garnet.server { @@ -114,12 +117,17 @@ public sealed class StoreWrapper /// /// Delegate for creating a new logical database /// - internal readonly Func createDatabasesDelegate; + internal readonly DatabaseCreatorDelegate createDatabasesDelegate; readonly bool allowMultiDb; internal ExpandableMap databases; - string activeDbIdsPath; + string databaseIdsFileName = "dbIds.dat"; + string aofDatabaseIdsPath; + string checkpointDatabaseIdsPath; + IStreamProvider databaseIdsStreamProvider; + + public delegate GarnetDatabase DatabaseCreatorDelegate(int dbId, out string storeCheckpointDir, out string aofDir); /// /// Constructor @@ -128,7 +136,7 @@ public StoreWrapper( string version, string redisProtocolVersion, IGarnetServer server, - Func createsDatabaseDelegate, + DatabaseCreatorDelegate createsDatabaseDelegate, CustomCommandManager customCommandManager, GarnetServerOptions serverOptions, AccessControlList accessControlList = null, @@ -154,11 +162,10 @@ public StoreWrapper( this.GarnetObjectSerializer = new GarnetObjectSerializer(this.customCommandManager); this.loggingFrequncy = TimeSpan.FromSeconds(serverOptions.LoggingFrequency); - var checkpointDir = serverOptions.CheckpointDir ?? serverOptions.LogDir; - activeDbIdsPath = Path.Combine(checkpointDir, "activeDbIds.dat"); - // Create initial stores for default database of index 0 - var db = createDatabasesDelegate(0); + var db = createDatabasesDelegate(0, out var storeCheckpointDir, out var aofPath); + checkpointDatabaseIdsPath = storeCheckpointDir == null ? null : Path.Combine(storeCheckpointDir, databaseIdsFileName); + aofDatabaseIdsPath = aofPath == null ? null : Path.Combine(aofPath, databaseIdsFileName); this.allowMultiDb = this.serverOptions.MaxDatabases > 1; @@ -167,6 +174,11 @@ public StoreWrapper( if (!databases.TrySetValue(0, ref db)) throw new GarnetException("Failed to set initial database in databases map"); + if (allowMultiDb) + { + databaseIdsStreamProvider = serverOptions.StreamProviderCreator(); + } + // Set fields to default database this.store = db.MainStore; this.objectStore = db.ObjectStore; @@ -266,7 +278,7 @@ internal FunctionsState CreateFunctionsState(int dbId = 0) if (dbId == 0) return new(appendOnlyFile, versionMap, customCommandManager, null, objectStoreSizeTracker, GarnetObjectSerializer); - if (!databases.TryGetOrSet(dbId, () => createDatabasesDelegate(dbId), out var db, out _)) + if (!databases.TryGetOrSet(dbId, () => createDatabasesDelegate(dbId, out _, out _), out var db, out _)) throw new GarnetException($"Database with ID {dbId} was not found."); return new(db.AppendOnlyFile, db.VersionMap, customCommandManager, null, db.ObjectSizeTracker, @@ -318,14 +330,17 @@ public void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStore } else { - using (var reader = new BinaryReader(File.Open(activeDbIdsPath, FileMode.Open))) + if (allowMultiDb && checkpointDatabaseIdsPath != null) { - while (reader.BaseStream.Position < reader.BaseStream.Length) + using var stream = databaseIdsStreamProvider.Read(checkpointDatabaseIdsPath); + using var streamReader = new BinaryReader(stream); + var idsCount = streamReader.ReadInt32(); + while (streamReader.BaseStream.Position < streamReader.BaseStream.Length) { - var dbId = reader.ReadInt32(); + var dbId = streamReader.ReadInt32(); if (dbId == 0) continue; - var db = createDatabasesDelegate(dbId); + var db = createDatabasesDelegate(dbId, out _, out _); databases.TrySetValue(dbId, ref db); } } @@ -470,15 +485,32 @@ async Task CommitTask(int commitFrequencyMs, ILogger logger = null, Cancellation var databasesMapSize = databases.ActualSize; var databasesMapSnapshot = databases.Map; - for (var i = 0; i < databasesMapSize; i++) + var activeDbIds = allowMultiDb ? new List() : null; + + for (var dbId = 0; dbId < databasesMapSize; dbId++) { - var db = databasesMapSnapshot[i]; + var db = databasesMapSnapshot[dbId]; if (db.Equals(default(GarnetDatabase))) continue; await db.AppendOnlyFile.CommitAsync(null, token); + + if (allowMultiDb && dbId != 0) + activeDbIds!.Add(dbId); } await Task.Delay(commitFrequencyMs, token); + + if (allowMultiDb && aofDatabaseIdsPath != null && activeDbIds!.Count > 0) + { + var dbIdsWithLength = new int[activeDbIds.Count + 1]; + dbIdsWithLength[0] = activeDbIds.Count; + activeDbIds.CopyTo(dbIdsWithLength, 1); + + var dbIdData = new byte[sizeof(int) * dbIdsWithLength.Length]; + + Buffer.BlockCopy(dbIdsWithLength, 0, dbIdData, 0, dbIdData.Length); + databaseIdsStreamProvider.Write(aofDatabaseIdsPath, dbIdData); + } } } } @@ -914,7 +946,8 @@ private async Task CheckpointTask(StoreType storeType, ILogger logger = null) var databasesMapSize = databases.ActualSize; var databasesMapSnapshot = databases.Map; - var activeDbIds = new List(); + var activeDbIds = allowMultiDb ? new List() : null; + for (var dbId = 0; dbId < databasesMapSize; dbId++) { var db = databasesMapSnapshot[dbId]; @@ -945,13 +978,15 @@ private async Task CheckpointTask(StoreType storeType, ILogger logger = null) db.LastSaveObjectStoreTailAddress = lastSaveObjectStoreTailAddress; } - activeDbIds.Add(dbId); + if(allowMultiDb && dbId != 0) + activeDbIds!.Add(dbId); } - await using (var bw = new BinaryWriter(File.Open(activeDbIdsPath, FileMode.Create))) + if (allowMultiDb && checkpointDatabaseIdsPath != null && activeDbIds!.Count > 0) { - foreach (var dbIds in activeDbIds) - bw.Write(dbIds); + var dbIdData = new byte[sizeof(int) * activeDbIds.Count]; + Buffer.BlockCopy(activeDbIds.ToArray(), 0, dbIdData, 0, dbIdData.Length); + databaseIdsStreamProvider.Write(checkpointDatabaseIdsPath, dbIdData); } lastSaveTime = DateTimeOffset.UtcNow; @@ -1092,7 +1127,7 @@ public bool TryGetOrSetDatabase(int dbId, out GarnetDatabase db) { db = default; - if (!allowMultiDb || !databases.TryGetOrSet(dbId, () => createDatabasesDelegate(dbId), out db, out var added)) + if (!allowMultiDb || !databases.TryGetOrSet(dbId, () => createDatabasesDelegate(dbId, out _, out _), out db, out var added)) return false; if (added) diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index b2022e0165..c70d064e6c 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -17,7 +17,7 @@ public class MultiDatabaseTests public void Setup() { TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); - server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, enableAOF: true); server.Start(); } @@ -236,6 +236,54 @@ public void MultiDatabaseSaveRecoverRawStringTest() } } + [Test] + public void MultiDatabaseAofRecoverObjectTest() + { + var db1Key = "db1:key1"; + var db2Key = "db2:key1"; + var db1data = new SortedSetEntry[]{ new("db1:a", 1), new("db1:b", 2), new("db1:c", 3) }; + var db2data = new SortedSetEntry[]{ new("db2:a", -1), new("db2:b", -2), new("db2:c", -3) }; + + using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig())) + { + var db1 = redis.GetDatabase(0); + var added = db1.SortedSetAdd(db1Key, db1data); + ClassicAssert.AreEqual(3, added); + + var score = db1.SortedSetScore(db1Key, "db1:a"); + ClassicAssert.True(score.HasValue); + ClassicAssert.AreEqual(1, score.Value); + + var db2 = redis.GetDatabase(1); + added = db2.SortedSetAdd(db2Key, db2data); + ClassicAssert.AreEqual(3, added); + + score = db2.SortedSetScore(db2Key, "db2:a"); + ClassicAssert.True(score.HasValue); + ClassicAssert.AreEqual(-1, score.Value); + } + + server.Store.CommitAOF(true); + server.Dispose(false); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, tryRecover: true, enableAOF: true); + server.Start(); + + using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig())) + { + var db1 = redis.GetDatabase(0); + + var score = db1.SortedSetScore(db1Key, "db1:a"); + ClassicAssert.True(score.HasValue); + ClassicAssert.AreEqual(1, score.Value); + + var db2 = redis.GetDatabase(1); + + score = db2.SortedSetScore(db2Key, "db2:a"); + ClassicAssert.True(score.HasValue); + ClassicAssert.AreEqual(-1, score.Value); + } + } + [TearDown] public void TearDown() { From 6bca54c488522c958386ace4a0b936268f856095 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Fri, 31 Jan 2025 16:10:53 -0700 Subject: [PATCH 08/82] wip --- libs/server/Servers/StoreApi.cs | 3 +- libs/server/StoreWrapper.cs | 102 +++++++++++++++++-------- test/Garnet.test/MultiDatabaseTests.cs | 2 +- 3 files changed, 74 insertions(+), 33 deletions(-) diff --git a/libs/server/Servers/StoreApi.cs b/libs/server/Servers/StoreApi.cs index c5b527e849..5b20cc5ef0 100644 --- a/libs/server/Servers/StoreApi.cs +++ b/libs/server/Servers/StoreApi.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; namespace Garnet.server { @@ -26,7 +27,7 @@ public StoreApi(StoreWrapper storeWrapper) /// Commit AOF /// /// - public void CommitAOF(bool spinWait = false) => storeWrapper.appendOnlyFile?.Commit(spinWait); + public void CommitAOF(bool spinWait = false) => storeWrapper.CommitAOF(spinWait); /// /// Wait for commit diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 573fed5f44..e785185cb1 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.ComponentModel.Design; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Net; using System.Net.Sockets; @@ -331,19 +332,7 @@ public void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStore else { if (allowMultiDb && checkpointDatabaseIdsPath != null) - { - using var stream = databaseIdsStreamProvider.Read(checkpointDatabaseIdsPath); - using var streamReader = new BinaryReader(stream); - var idsCount = streamReader.ReadInt32(); - while (streamReader.BaseStream.Position < streamReader.BaseStream.Length) - { - var dbId = streamReader.ReadInt32(); - if (dbId == 0) continue; - - var db = createDatabasesDelegate(dbId, out _, out _); - databases.TrySetValue(dbId, ref db); - } - } + RecoverDatabases(checkpointDatabaseIdsPath); var databasesMapSize = databases.ActualSize; var databasesMapSnapshot = databases.Map; @@ -370,14 +359,55 @@ public void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStore } } + private void WriteDatabaseIdsSnapshot(IList activeDbIds, string path) + { + var dbIdsWithLength = new int[activeDbIds.Count + 1]; + dbIdsWithLength[0] = activeDbIds.Count; + activeDbIds.CopyTo(dbIdsWithLength, 1); + + var dbIdData = new byte[sizeof(int) * dbIdsWithLength.Length]; + + Buffer.BlockCopy(dbIdsWithLength, 0, dbIdData, 0, dbIdData.Length); + databaseIdsStreamProvider.Write(path, dbIdData); + } + + private void RecoverDatabases(string path) + { + using var stream = databaseIdsStreamProvider.Read(path); + using var streamReader = new BinaryReader(stream); + + if (streamReader.BaseStream.Length > 0) + { + var idsCount = streamReader.ReadInt32(); + var dbIds = new int[idsCount]; + var dbIdData = streamReader.ReadBytes((int)streamReader.BaseStream.Length); + Buffer.BlockCopy(dbIdData, 0, dbIds, 0, dbIdData.Length); + + foreach (var dbId in dbIds) + { + var db = createDatabasesDelegate(dbId, out _, out _); + databases.TrySetValue(dbId, ref db); + } + } + } + /// /// Recover AOF /// public void RecoverAOF() { - if (appendOnlyFile == null) return; - appendOnlyFile.Recover(); - logger?.LogInformation("Recovered AOF: begin address = {beginAddress}, tail address = {tailAddress}", appendOnlyFile.BeginAddress, appendOnlyFile.TailAddress); + if (allowMultiDb && checkpointDatabaseIdsPath != null) + RecoverDatabases(checkpointDatabaseIdsPath); + + var databasesMapSize = databases.ActualSize; + var databasesMapSnapshot = databases.Map; + for (var i = 0; i < databasesMapSize; i++) + { + var db = databasesMapSnapshot[i]; + if (db.AppendOnlyFile == null) continue; + db.AppendOnlyFile.Recover(); + logger?.LogInformation("Recovered AOF: begin address = {beginAddress}, tail address = {tailAddress}", db.AppendOnlyFile.BeginAddress, db.AppendOnlyFile.TailAddress); + } } /// @@ -501,16 +531,7 @@ async Task CommitTask(int commitFrequencyMs, ILogger logger = null, Cancellation await Task.Delay(commitFrequencyMs, token); if (allowMultiDb && aofDatabaseIdsPath != null && activeDbIds!.Count > 0) - { - var dbIdsWithLength = new int[activeDbIds.Count + 1]; - dbIdsWithLength[0] = activeDbIds.Count; - activeDbIds.CopyTo(dbIdsWithLength, 1); - - var dbIdData = new byte[sizeof(int) * dbIdsWithLength.Length]; - - Buffer.BlockCopy(dbIdsWithLength, 0, dbIdData, 0, dbIdData.Length); - databaseIdsStreamProvider.Write(aofDatabaseIdsPath, dbIdData); - } + WriteDatabaseIdsSnapshot(activeDbIds, aofDatabaseIdsPath); } } } @@ -734,6 +755,29 @@ void CompactionCommitAof() } } + internal void CommitAOF(bool spinWait) + { + var databasesMapSize = databases.ActualSize; + var databasesMapSnapshot = databases.Map; + + var activeDbIds = allowMultiDb ? new List() : null; + + for (var dbId = 0; dbId < databasesMapSize; dbId++) + { + var db = databasesMapSnapshot[dbId]; + if (db.Equals(default(GarnetDatabase))) continue; + + if (db.AppendOnlyFile == null) continue; + db.AppendOnlyFile.Commit(spinWait); + + if (allowMultiDb && dbId != 0) + activeDbIds!.Add(dbId); + } + + if (allowMultiDb && aofDatabaseIdsPath != null && activeDbIds!.Count > 0) + WriteDatabaseIdsSnapshot(activeDbIds, aofDatabaseIdsPath); + } + internal void Start() { monitor?.Start(); @@ -983,11 +1027,7 @@ private async Task CheckpointTask(StoreType storeType, ILogger logger = null) } if (allowMultiDb && checkpointDatabaseIdsPath != null && activeDbIds!.Count > 0) - { - var dbIdData = new byte[sizeof(int) * activeDbIds.Count]; - Buffer.BlockCopy(activeDbIds.ToArray(), 0, dbIdData, 0, dbIdData.Length); - databaseIdsStreamProvider.Write(checkpointDatabaseIdsPath, dbIdData); - } + WriteDatabaseIdsSnapshot(activeDbIds, checkpointDatabaseIdsPath); lastSaveTime = DateTimeOffset.UtcNow; } diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index c70d064e6c..6f03d7e2e0 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -17,7 +17,7 @@ public class MultiDatabaseTests public void Setup() { TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); - server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, enableAOF: true); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, enableAOF: true, lowMemory:true); server.Start(); } From 21aa35a4a284280f5e206a0415155aef5abd9fef Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Fri, 31 Jan 2025 17:55:28 -0700 Subject: [PATCH 09/82] wip --- libs/server/AOF/AofProcessor.cs | 12 +--- libs/server/GarnetDatabase.cs | 2 + libs/server/StoreWrapper.cs | 104 +++++++++++++++++++++++--------- 3 files changed, 80 insertions(+), 38 deletions(-) diff --git a/libs/server/AOF/AofProcessor.cs b/libs/server/AOF/AofProcessor.cs index c855b95650..e5e311163a 100644 --- a/libs/server/AOF/AofProcessor.cs +++ b/libs/server/AOF/AofProcessor.cs @@ -68,19 +68,9 @@ public AofProcessor( ReplicationOffset = 0; - var replayAofStoreWrapper = new StoreWrapper( - storeWrapper.version, - storeWrapper.redisProtocolVersion, - null, - storeWrapper.createDatabasesDelegate, - storeWrapper.customCommandManager, - //recordToAof ? storeWrapper.appendOnlyFile : null, - storeWrapper.serverOptions, - accessControlList: storeWrapper.accessControlList, - loggerFactory: storeWrapper.loggerFactory); + var replayAofStoreWrapper = new StoreWrapper(storeWrapper, recordToAof); this.respServerSession = new RespServerSession(0, networkSender: null, storeWrapper: replayAofStoreWrapper, subscribeBroker: null, authenticator: null, enableScripts: false); - var session = respServerSession.storageSession.basicContext.Session; basicContext = session.BasicContext; var objectStoreSession = respServerSession.storageSession.objectStoreBasicContext.Session; diff --git a/libs/server/GarnetDatabase.cs b/libs/server/GarnetDatabase.cs index a3166086f4..d835801141 100644 --- a/libs/server/GarnetDatabase.cs +++ b/libs/server/GarnetDatabase.cs @@ -76,6 +76,8 @@ public GarnetDatabase(TsavoriteKV MainStore == null; + public void Dispose() { if (disposed) return; diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index e785185cb1..49aadc1286 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -4,16 +4,13 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.ComponentModel.Design; using System.Diagnostics; -using System.Globalization; using System.IO; using System.Net; using System.Net.Sockets; -using System.Reflection; +using System.Reflection.Metadata.Ecma335; using System.Threading; using System.Threading.Tasks; -using System.Xml; using Garnet.common; using Garnet.server.ACL; using Garnet.server.Auth.Settings; @@ -140,6 +137,7 @@ public StoreWrapper( DatabaseCreatorDelegate createsDatabaseDelegate, CustomCommandManager customCommandManager, GarnetServerOptions serverOptions, + bool createDefaultDatabase = true, AccessControlList accessControlList = null, IClusterFactory clusterFactory = null, ILoggerFactory loggerFactory = null) @@ -148,10 +146,10 @@ public StoreWrapper( this.redisProtocolVersion = redisProtocolVersion; this.server = server; this.startupTime = DateTimeOffset.UtcNow.Ticks; - this.createDatabasesDelegate = createsDatabaseDelegate; this.serverOptions = serverOptions; - lastSaveTime = DateTimeOffset.FromUnixTimeSeconds(0); + this.lastSaveTime = DateTimeOffset.FromUnixTimeSeconds(0); this.customCommandManager = customCommandManager; + this.createDatabasesDelegate = createsDatabaseDelegate; this.monitor = serverOptions.MetricsSamplingFrequency > 0 ? new GarnetServerMonitor(this, serverOptions, server, loggerFactory?.CreateLogger("GarnetServerMonitor")) @@ -163,30 +161,29 @@ public StoreWrapper( this.GarnetObjectSerializer = new GarnetObjectSerializer(this.customCommandManager); this.loggingFrequncy = TimeSpan.FromSeconds(serverOptions.LoggingFrequency); - // Create initial stores for default database of index 0 - var db = createDatabasesDelegate(0, out var storeCheckpointDir, out var aofPath); - checkpointDatabaseIdsPath = storeCheckpointDir == null ? null : Path.Combine(storeCheckpointDir, databaseIdsFileName); - aofDatabaseIdsPath = aofPath == null ? null : Path.Combine(aofPath, databaseIdsFileName); - this.allowMultiDb = this.serverOptions.MaxDatabases > 1; - // Create databases map and set initial database + // Create databases map databases = new ExpandableMap(1, 0, this.serverOptions.MaxDatabases - 1); - if (!databases.TrySetValue(0, ref db)) - throw new GarnetException("Failed to set initial database in databases map"); + + // Create default database of index 0 (unless specified otherwise) + if (createDefaultDatabase) + { + var db = createDatabasesDelegate(0, out var storeCheckpointDir, out var aofPath); + checkpointDatabaseIdsPath = storeCheckpointDir == null ? null : Path.Combine(storeCheckpointDir, databaseIdsFileName); + aofDatabaseIdsPath = aofPath == null ? null : Path.Combine(aofPath, databaseIdsFileName); + + if (!databases.TrySetValue(0, ref db)) + throw new GarnetException("Failed to set initial database in databases map"); + + this.InitializeFieldsFromDatabase(databases.Map[0]); + } if (allowMultiDb) { databaseIdsStreamProvider = serverOptions.StreamProviderCreator(); } - // Set fields to default database - this.store = db.MainStore; - this.objectStore = db.ObjectStore; - this.objectStoreSizeTracker = db.ObjectSizeTracker; - this.appendOnlyFile = db.AppendOnlyFile; - this.versionMap = db.VersionMap; - if (logger != null) { var configMemoryLimit = (store.IndexSize * 64) + store.Log.MaxMemorySizeBytes + @@ -244,6 +241,53 @@ public StoreWrapper( run_id = Generator.CreateHexId(); } + /// + /// Copy Constructor + /// + /// Source instance + /// + public StoreWrapper(StoreWrapper storeWrapper, bool recordToAof) : this( + storeWrapper.version, + storeWrapper.redisProtocolVersion, + storeWrapper.server, + storeWrapper.createDatabasesDelegate, + storeWrapper.customCommandManager, + storeWrapper.serverOptions, + createDefaultDatabase: false, + storeWrapper.accessControlList, + null, + storeWrapper.loggerFactory) + { + this.clusterProvider = storeWrapper.clusterProvider; + this.CopyDatabases(storeWrapper, recordToAof); + } + + private void CopyDatabases(StoreWrapper src, bool recordToAof) + { + var databasesMapSize = src.databases.ActualSize; + var databasesMapSnapshot = src.databases.Map; + + for (var dbId = 0; dbId < databasesMapSize; dbId++) + { + var db = databasesMapSnapshot[dbId]; + var dbCopy = new GarnetDatabase(db.MainStore, db.ObjectStore, db.ObjectSizeTracker, + recordToAof ? db.AofDevice : null, recordToAof ? db.AppendOnlyFile : null); + databases.TrySetValue(dbId, ref dbCopy); + } + + InitializeFieldsFromDatabase(databases.Map[0]); + } + + private void InitializeFieldsFromDatabase(GarnetDatabase db) + { + // Set fields to default database + this.store = db.MainStore; + this.objectStore = db.ObjectStore; + this.objectStoreSizeTracker = db.ObjectSizeTracker; + this.appendOnlyFile = db.AppendOnlyFile; + this.versionMap = db.VersionMap; + } + /// /// Get IP /// @@ -383,8 +427,14 @@ private void RecoverDatabases(string path) var dbIdData = streamReader.ReadBytes((int)streamReader.BaseStream.Length); Buffer.BlockCopy(dbIdData, 0, dbIds, 0, dbIdData.Length); + var databasesMapSize = databases.ActualSize; + var databasesMapSnapshot = databases.Map; + foreach (var dbId in dbIds) { + if (dbId < databasesMapSize && !databasesMapSnapshot[dbId].IsDefault()) + continue; + var db = createDatabasesDelegate(dbId, out _, out _); databases.TrySetValue(dbId, ref db); } @@ -474,7 +524,7 @@ async Task AutoCheckpointBasedOnAofSizeLimit(long aofSizeLimit, CancellationToke for (var i = 0; i < databasesMapSize; i++) { var db = databasesMapSnapshot[i]; - if (db.Equals(default(GarnetDatabase))) continue; + if (db.IsDefault()) continue; var dbAofSize = db.AppendOnlyFile.TailAddress - db.AppendOnlyFile.BeginAddress; if (dbAofSize > aofSizeLimit) @@ -520,7 +570,7 @@ async Task CommitTask(int commitFrequencyMs, ILogger logger = null, Cancellation for (var dbId = 0; dbId < databasesMapSize; dbId++) { var db = databasesMapSnapshot[dbId]; - if (db.Equals(default(GarnetDatabase))) continue; + if (db.IsDefault()) continue; await db.AppendOnlyFile.CommitAsync(null, token); @@ -556,7 +606,7 @@ async Task CompactionTask(int compactionFrequencySecs, CancellationToken token = for (var i = 0; i < databasesMapSize; i++) { var db = databasesMapSnapshot[i]; - if (db.Equals(default(GarnetDatabase))) continue; + if (db.IsDefault()) continue; DoCompaction(ref db, serverOptions.CompactionMaxSegments, serverOptions.ObjectStoreCompactionMaxSegments, 1, serverOptions.CompactionType, serverOptions.CompactionForceDelete); } @@ -765,7 +815,7 @@ internal void CommitAOF(bool spinWait) for (var dbId = 0; dbId < databasesMapSize; dbId++) { var db = databasesMapSnapshot[dbId]; - if (db.Equals(default(GarnetDatabase))) continue; + if (db.IsDefault()) continue; if (db.AppendOnlyFile == null) continue; db.AppendOnlyFile.Commit(spinWait); @@ -826,7 +876,7 @@ private async void IndexAutoGrowTask(CancellationToken token) for (var i = 0; i < databasesMapSize; i++) { - if (databasesMapSnapshot[i].Equals(default(GarnetDatabase))) continue; + if (databasesMapSnapshot[i].IsDefault()) continue; databaseMainStoreIndexMaxedOut[i] = serverOptions.AdjustedIndexMaxCacheLines == 0; databaseObjectStoreIndexMaxedOut[i] = serverOptions.AdjustedObjectStoreIndexMaxCacheLines == 0; @@ -995,7 +1045,7 @@ private async Task CheckpointTask(StoreType storeType, ILogger logger = null) for (var dbId = 0; dbId < databasesMapSize; dbId++) { var db = databasesMapSnapshot[dbId]; - if (db.Equals(default(GarnetDatabase))) continue; + if (db.IsDefault()) continue; DoCompaction(ref db); var lastSaveStoreTailAddress = db.MainStore.Log.TailAddress; From e8f465ddcccd53f1cb07dd18c24300f171bf16dc Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 4 Feb 2025 18:01:19 -0800 Subject: [PATCH 10/82] aof --- libs/server/AOF/AofProcessor.cs | 50 ++++++++++++++------- libs/server/Resp/RespServerSession.cs | 5 ++- libs/server/StoreWrapper.cs | 18 ++++++-- test/Garnet.test/MultiDatabaseTests.cs | 60 +++++++++++++++++++++++--- 4 files changed, 106 insertions(+), 27 deletions(-) diff --git a/libs/server/AOF/AofProcessor.cs b/libs/server/AOF/AofProcessor.cs index e5e311163a..1e14b988ba 100644 --- a/libs/server/AOF/AofProcessor.cs +++ b/libs/server/AOF/AofProcessor.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Garnet.common; @@ -13,6 +14,7 @@ namespace Garnet.server { + using static System.Collections.Specialized.BitVector32; using MainStoreAllocator = SpanByteAllocator>; using MainStoreFunctions = StoreFunctions; @@ -31,6 +33,8 @@ public sealed unsafe partial class AofProcessor private readonly ObjectInput objectStoreInput; private readonly CustomProcedureInput customProcInput; private readonly SessionParseState parseState; + + int activeDbId; /// /// Replication offset @@ -40,12 +44,12 @@ public sealed unsafe partial class AofProcessor /// /// Session for main store /// - readonly BasicContext basicContext; + BasicContext basicContext; /// /// Session for object store /// - readonly BasicContext objectStoreBasicContext; + BasicContext objectStoreBasicContext; readonly Dictionary> inflightTxns; readonly byte[] buffer; @@ -53,7 +57,6 @@ public sealed unsafe partial class AofProcessor readonly byte* bufferPtr; readonly ILogger logger; - readonly bool recordToAof; /// /// Create new AOF processor @@ -64,18 +67,13 @@ public AofProcessor( ILogger logger = null) { this.storeWrapper = storeWrapper; - this.recordToAof = recordToAof; - ReplicationOffset = 0; var replayAofStoreWrapper = new StoreWrapper(storeWrapper, recordToAof); + this.activeDbId = 0; this.respServerSession = new RespServerSession(0, networkSender: null, storeWrapper: replayAofStoreWrapper, subscribeBroker: null, authenticator: null, enableScripts: false); - var session = respServerSession.storageSession.basicContext.Session; - basicContext = session.BasicContext; - var objectStoreSession = respServerSession.storageSession.objectStoreBasicContext.Session; - if (objectStoreSession is not null) - objectStoreBasicContext = objectStoreSession.BasicContext; + SwitchActiveDatabaseContext(0, true); parseState.Initialize(); storeInput.parseState = parseState; @@ -102,21 +100,24 @@ public void Dispose() /// /// Recover store using AOF /// - public unsafe void Recover(long untilAddress = -1) + public unsafe void Recover(int dbId = 0, long untilAddress = -1) { logger?.LogInformation("Begin AOF recovery"); - RecoverReplay(untilAddress); + RecoverReplay(dbId, untilAddress); } MemoryResult output = default; - private unsafe void RecoverReplay(long untilAddress) + private unsafe void RecoverReplay(int dbId, long untilAddress) { logger?.LogInformation("Begin AOF replay"); try { int count = 0; - if (untilAddress == -1) untilAddress = storeWrapper.appendOnlyFile.TailAddress; - using var scan = storeWrapper.appendOnlyFile.Scan(storeWrapper.appendOnlyFile.BeginAddress, untilAddress); + var appendOnlyFile = storeWrapper.databases.Map[dbId].AppendOnlyFile; + SwitchActiveDatabaseContext(dbId); + + if (untilAddress == -1) untilAddress = appendOnlyFile.TailAddress; + using var scan = appendOnlyFile.Scan(appendOnlyFile.BeginAddress, untilAddress); while (scan.GetNext(MemoryPool.Shared, out var entry, out var length, out _, out long nextAofAddress)) { @@ -264,6 +265,25 @@ private unsafe bool ReplayOp(byte* entryPtr) return true; } + private void SwitchActiveDatabaseContext(int dbId, bool initialSetup = false) + { + if (respServerSession.activeDbId != dbId) + { + var switchDbSuccessful = respServerSession.TrySwitchActiveDatabaseSession(dbId); + Debug.Assert(switchDbSuccessful); + } + + if (this.activeDbId != dbId || initialSetup) + { + var session = respServerSession.storageSession.basicContext.Session; + basicContext = session.BasicContext; + var objectStoreSession = respServerSession.storageSession.objectStoreBasicContext.Session; + if (objectStoreSession is not null) + objectStoreBasicContext = objectStoreSession.BasicContext; + this.activeDbId = dbId; + } + } + unsafe void RunStoredProc(byte id, CustomProcedureInput customProcInput, byte* ptr) { var curr = ptr + sizeof(AofHeader); diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index 22cf58634f..7bd79d583c 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -96,9 +96,9 @@ internal sealed unsafe partial class RespServerSession : ServerSessionBase readonly IGarnetAuthenticator _authenticator; + internal int activeDbId; readonly bool allowMultiDb; readonly int maxDbs; - int activeDbId; ExpandableMap databaseSessions; /// @@ -1240,7 +1240,8 @@ private unsafe ObjectOutputHeader ProcessOutputWithHeader(SpanByteAndMemory outp return header; } - private bool TrySwitchActiveDatabaseSession(int dbId) + + internal bool TrySwitchActiveDatabaseSession(int dbId) { if (!allowMultiDb) return false; diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 49aadc1286..8b51e5af5c 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -446,8 +446,8 @@ private void RecoverDatabases(string path) /// public void RecoverAOF() { - if (allowMultiDb && checkpointDatabaseIdsPath != null) - RecoverDatabases(checkpointDatabaseIdsPath); + if (allowMultiDb && aofDatabaseIdsPath != null) + RecoverDatabases(aofDatabaseIdsPath); var databasesMapSize = databases.ActualSize; var databasesMapSnapshot = databases.Map; @@ -494,9 +494,19 @@ public long ReplayAOF(long untilAddress = -1) // When replaying AOF we do not want to write record again to AOF. // So initialize local AofProcessor with recordToAof: false. var aofProcessor = new AofProcessor(this, recordToAof: false, logger); - aofProcessor.Recover(untilAddress); + + var databasesMapSize = databases.ActualSize; + var databasesMapSnapshot = databases.Map; + + for (var dbId = 0; dbId < databasesMapSize; dbId++) + { + if (databasesMapSnapshot[dbId].IsDefault()) continue; + aofProcessor.Recover(dbId, dbId == 0 ? untilAddress : -1); + if (dbId == 0) + replicationOffset = aofProcessor.ReplicationOffset; + } + aofProcessor.Dispose(); - replicationOffset = aofProcessor.ReplicationOffset; lastSaveTime = DateTimeOffset.UtcNow; } catch (Exception ex) diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index 6f03d7e2e0..5137ca252e 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -135,8 +135,8 @@ public void MultiDatabaseBasicSelectTestLC() actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); ClassicAssert.AreEqual(expectedResponse, actualValue); - response = lightClientRequest.SendCommand($"SPOP {db2Key2}", 2); - expectedResponse = "$8\r\ndb2:val2\r\n"; + response = lightClientRequest.SendCommand($"SISMEMBER {db2Key2} db2:val2"); + expectedResponse = ":1\r\n"; actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); ClassicAssert.AreEqual(expectedResponse, actualValue); } @@ -236,6 +236,54 @@ public void MultiDatabaseSaveRecoverRawStringTest() } } + [Test] + public void MultiDatabaseAofRecoverRawStringTest() + { + var db1Key = "db1:key1"; + var db2Key = "db2:key1"; + var db1data = new RedisValue("db1:a"); + var db2data = new RedisValue("db2:a"); + + using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig())) + { + var db1 = redis.GetDatabase(0); + var result = db1.StringSet(db1Key, db1data); + ClassicAssert.IsTrue(result); + + var value = db1.StringGet(db1Key); + ClassicAssert.IsTrue(value.HasValue); + ClassicAssert.AreEqual(db1data, value.ToString()); + + var db2 = redis.GetDatabase(1); + result = db2.StringSet(db2Key, db2data); + ClassicAssert.IsTrue(result); + + value = db2.StringGet(db2Key); + ClassicAssert.IsTrue(value.HasValue); + ClassicAssert.AreEqual(db2data, value.ToString()); + } + + server.Store.CommitAOF(true); + server.Dispose(false); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, tryRecover: true, enableAOF: true); + server.Start(); + + using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig())) + { + var db1 = redis.GetDatabase(0); + + var value = db1.StringGet(db1Key); + ClassicAssert.IsTrue(value.HasValue); + ClassicAssert.AreEqual(db1data, value.ToString()); + + var db2 = redis.GetDatabase(1); + + value = db2.StringGet(db2Key); + ClassicAssert.IsTrue(value.HasValue); + ClassicAssert.AreEqual(db2data, value.ToString()); + } + } + [Test] public void MultiDatabaseAofRecoverObjectTest() { @@ -251,7 +299,7 @@ public void MultiDatabaseAofRecoverObjectTest() ClassicAssert.AreEqual(3, added); var score = db1.SortedSetScore(db1Key, "db1:a"); - ClassicAssert.True(score.HasValue); + ClassicAssert.IsTrue(score.HasValue); ClassicAssert.AreEqual(1, score.Value); var db2 = redis.GetDatabase(1); @@ -259,7 +307,7 @@ public void MultiDatabaseAofRecoverObjectTest() ClassicAssert.AreEqual(3, added); score = db2.SortedSetScore(db2Key, "db2:a"); - ClassicAssert.True(score.HasValue); + ClassicAssert.IsTrue(score.HasValue); ClassicAssert.AreEqual(-1, score.Value); } @@ -273,13 +321,13 @@ public void MultiDatabaseAofRecoverObjectTest() var db1 = redis.GetDatabase(0); var score = db1.SortedSetScore(db1Key, "db1:a"); - ClassicAssert.True(score.HasValue); + ClassicAssert.IsTrue(score.HasValue); ClassicAssert.AreEqual(1, score.Value); var db2 = redis.GetDatabase(1); score = db2.SortedSetScore(db2Key, "db2:a"); - ClassicAssert.True(score.HasValue); + ClassicAssert.IsTrue(score.HasValue); ClassicAssert.AreEqual(-1, score.Value); } } From cabd3a69db217f06e4db6ae35d11095e0bf4631d Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 4 Feb 2025 19:45:00 -0800 Subject: [PATCH 11/82] wip --- libs/server/StoreWrapper.cs | 91 +++++++++++++++++++++++-------------- 1 file changed, 57 insertions(+), 34 deletions(-) diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 8b51e5af5c..e9e2d3a148 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -2,13 +2,17 @@ // Licensed under the MIT license. using System; +using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Net; using System.Net.Sockets; using System.Reflection.Metadata.Ecma335; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Garnet.common; @@ -403,11 +407,11 @@ public void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStore } } - private void WriteDatabaseIdsSnapshot(IList activeDbIds, string path) + private void WriteDatabaseIdsSnapshot(int[] activeDbIds, int offset, int actualLength, string path) { - var dbIdsWithLength = new int[activeDbIds.Count + 1]; - dbIdsWithLength[0] = activeDbIds.Count; - activeDbIds.CopyTo(dbIdsWithLength, 1); + var dbIdsWithLength = new int[actualLength + 1]; + dbIdsWithLength[0] = actualLength; + Array.Copy(activeDbIds, offset, dbIdsWithLength, 1, actualLength); var dbIdData = new byte[sizeof(int) * dbIdsWithLength.Length]; @@ -561,6 +565,9 @@ async Task CommitTask(int commitFrequencyMs, ILogger logger = null, Cancellation { try { + int[] activeDbIds = null; + Task[] tasks = null; + while (true) { if (token.IsCancellationRequested) break; @@ -573,25 +580,17 @@ async Task CommitTask(int commitFrequencyMs, ILogger logger = null, Cancellation else { var databasesMapSize = databases.ActualSize; - var databasesMapSnapshot = databases.Map; - - var activeDbIds = allowMultiDb ? new List() : null; - for (var dbId = 0; dbId < databasesMapSize; dbId++) + if (!allowMultiDb || databasesMapSize == 1) { - var db = databasesMapSnapshot[dbId]; - if (db.IsDefault()) continue; - - await db.AppendOnlyFile.CommitAsync(null, token); - - if (allowMultiDb && dbId != 0) - activeDbIds!.Add(dbId); + await appendOnlyFile.CommitAsync(null, token); + } + else + { + MultiDatabaseCommit(ref activeDbIds, ref tasks, token); } await Task.Delay(commitFrequencyMs, token); - - if (allowMultiDb && aofDatabaseIdsPath != null && activeDbIds!.Count > 0) - WriteDatabaseIdsSnapshot(activeDbIds, aofDatabaseIdsPath); } } } @@ -601,6 +600,38 @@ async Task CommitTask(int commitFrequencyMs, ILogger logger = null, Cancellation } } + void MultiDatabaseCommit(ref int[] activeDbIds, ref Task[] tasks, CancellationToken token) + { + var databasesMapSize = databases.ActualSize; + var databasesMapSnapshot = databases.Map; + if (activeDbIds == null || activeDbIds.Length < databasesMapSize) + { + activeDbIds = new int[databasesMapSize]; + tasks = new Task[databasesMapSize]; + } + + var dbIdsIdx = 0; + for (var dbId = 0; dbId < databasesMapSize; dbId++) + { + var db = databasesMapSnapshot[dbId]; + if (db.IsDefault()) continue; + + tasks[dbIdsIdx] = db.AppendOnlyFile.CommitAsync(null, token).AsTask(); + activeDbIds[dbIdsIdx] = dbId; + + dbIdsIdx++; + } + + for (var i = 0; i < dbIdsIdx; i++) + { + tasks[i].Wait(token); + tasks[i] = null; + } + + if (aofDatabaseIdsPath != null && dbIdsIdx != 0) + WriteDatabaseIdsSnapshot(activeDbIds, 1, dbIdsIdx - 1, aofDatabaseIdsPath); + } + async Task CompactionTask(int compactionFrequencySecs, CancellationToken token = default) { Debug.Assert(compactionFrequencySecs > 0); @@ -818,24 +849,16 @@ void CompactionCommitAof() internal void CommitAOF(bool spinWait) { var databasesMapSize = databases.ActualSize; - var databasesMapSnapshot = databases.Map; - - var activeDbIds = allowMultiDb ? new List() : null; - - for (var dbId = 0; dbId < databasesMapSize; dbId++) + if (!allowMultiDb || databasesMapSize == 1) { - var db = databasesMapSnapshot[dbId]; - if (db.IsDefault()) continue; - - if (db.AppendOnlyFile == null) continue; - db.AppendOnlyFile.Commit(spinWait); - - if (allowMultiDb && dbId != 0) - activeDbIds!.Add(dbId); + appendOnlyFile.Commit(spinWait); + return; } - if (allowMultiDb && aofDatabaseIdsPath != null && activeDbIds!.Count > 0) - WriteDatabaseIdsSnapshot(activeDbIds, aofDatabaseIdsPath); + int[] activeDbIds = null; + Task[] tasks = null; + + MultiDatabaseCommit(ref activeDbIds, ref tasks, CancellationToken.None); } internal void Start() @@ -1087,7 +1110,7 @@ private async Task CheckpointTask(StoreType storeType, ILogger logger = null) } if (allowMultiDb && checkpointDatabaseIdsPath != null && activeDbIds!.Count > 0) - WriteDatabaseIdsSnapshot(activeDbIds, checkpointDatabaseIdsPath); + WriteDatabaseIdsSnapshot(activeDbIds.ToArray(), 0, activeDbIds.Count, checkpointDatabaseIdsPath); lastSaveTime = DateTimeOffset.UtcNow; } From cf85d17121291067f13959e49ec1bc2119170a5a Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Wed, 5 Feb 2025 13:20:53 -0800 Subject: [PATCH 12/82] wip --- libs/server/AOF/AofProcessor.cs | 4 +- libs/server/GarnetDatabase.cs | 5 + libs/server/StoreWrapper.cs | 417 ++++++++++++++++++++++---------- 3 files changed, 303 insertions(+), 123 deletions(-) diff --git a/libs/server/AOF/AofProcessor.cs b/libs/server/AOF/AofProcessor.cs index 1e14b988ba..f8d683944a 100644 --- a/libs/server/AOF/AofProcessor.cs +++ b/libs/server/AOF/AofProcessor.cs @@ -113,7 +113,9 @@ private unsafe void RecoverReplay(int dbId, long untilAddress) try { int count = 0; - var appendOnlyFile = storeWrapper.databases.Map[dbId].AppendOnlyFile; + storeWrapper.TryGetOrSetDatabase(dbId, out var db); + var appendOnlyFile = db.AppendOnlyFile; + SwitchActiveDatabaseContext(dbId); if (untilAddress == -1) untilAddress = appendOnlyFile.TailAddress; diff --git a/libs/server/GarnetDatabase.cs b/libs/server/GarnetDatabase.cs index d835801141..b777797c73 100644 --- a/libs/server/GarnetDatabase.cs +++ b/libs/server/GarnetDatabase.cs @@ -60,6 +60,11 @@ public struct GarnetDatabase : IDisposable /// public long LastSaveObjectStoreTailAddress; + /// + /// Last time checkpoint of database was taken + /// + public DateTimeOffset LastSaveTime; + bool disposed = false; public GarnetDatabase(TsavoriteKV mainStore, diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index e9e2d3a148..58c9f89bc8 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -2,17 +2,12 @@ // Licensed under the MIT license. using System; -using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; using System.Net; using System.Net.Sockets; -using System.Reflection.Metadata.Ecma335; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Garnet.common; @@ -114,7 +109,7 @@ public sealed class StoreWrapper /// /// Number of current logical databases /// - public int databaseCount = 1; + public int databaseCount; /// /// Delegate for creating a new logical database @@ -122,7 +117,12 @@ public sealed class StoreWrapper internal readonly DatabaseCreatorDelegate createDatabasesDelegate; readonly bool allowMultiDb; - internal ExpandableMap databases; + ExpandableMap databases; + int[] activeDbIds = null; + int activeDbIdsLength; + Task[] checkpointTasks = null; + Task[] aofTasks = null; + object activeDbIdsLock = new(); string databaseIdsFileName = "dbIds.dat"; string aofDatabaseIdsPath; @@ -177,7 +177,7 @@ public StoreWrapper( checkpointDatabaseIdsPath = storeCheckpointDir == null ? null : Path.Combine(storeCheckpointDir, databaseIdsFileName); aofDatabaseIdsPath = aofPath == null ? null : Path.Combine(aofPath, databaseIdsFileName); - if (!databases.TrySetValue(0, ref db)) + if (!this.TrySetDatabase(0, ref db)) throw new GarnetException("Failed to set initial database in databases map"); this.InitializeFieldsFromDatabase(databases.Map[0]); @@ -276,7 +276,7 @@ private void CopyDatabases(StoreWrapper src, bool recordToAof) var db = databasesMapSnapshot[dbId]; var dbCopy = new GarnetDatabase(db.MainStore, db.ObjectStore, db.ObjectSizeTracker, recordToAof ? db.AofDevice : null, recordToAof ? db.AppendOnlyFile : null); - databases.TrySetValue(dbId, ref dbCopy); + this.TrySetDatabase(dbId, ref dbCopy); } InitializeFieldsFromDatabase(databases.Map[0]); @@ -327,9 +327,9 @@ internal FunctionsState CreateFunctionsState(int dbId = 0) if (dbId == 0) return new(appendOnlyFile, versionMap, customCommandManager, null, objectStoreSizeTracker, GarnetObjectSerializer); - if (!databases.TryGetOrSet(dbId, () => createDatabasesDelegate(dbId, out _, out _), out var db, out _)) + if (!this.TryGetOrSetDatabase(dbId, out var db)) throw new GarnetException($"Database with ID {dbId} was not found."); - + return new(db.AppendOnlyFile, db.VersionMap, customCommandManager, null, db.ObjectSizeTracker, GarnetObjectSerializer); } @@ -407,11 +407,14 @@ public void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStore } } - private void WriteDatabaseIdsSnapshot(int[] activeDbIds, int offset, int actualLength, string path) + private void WriteDatabaseIdsSnapshot(string path) { - var dbIdsWithLength = new int[actualLength + 1]; - dbIdsWithLength[0] = actualLength; - Array.Copy(activeDbIds, offset, dbIdsWithLength, 1, actualLength); + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + + var dbIdsWithLength = new int[activeDbIdsSize]; + dbIdsWithLength[0] = activeDbIdsSize - 1; + Array.Copy(activeDbIdsSnapshot, 1, dbIdsWithLength, 1, activeDbIdsSize - 1); var dbIdData = new byte[sizeof(int) * dbIdsWithLength.Length]; @@ -439,8 +442,7 @@ private void RecoverDatabases(string path) if (dbId < databasesMapSize && !databasesMapSnapshot[dbId].IsDefault()) continue; - var db = createDatabasesDelegate(dbId, out _, out _); - databases.TrySetValue(dbId, ref db); + this.TryGetOrSetDatabase(dbId, out _); } } } @@ -499,19 +501,28 @@ public long ReplayAOF(long untilAddress = -1) // So initialize local AofProcessor with recordToAof: false. var aofProcessor = new AofProcessor(this, recordToAof: false, logger); - var databasesMapSize = databases.ActualSize; var databasesMapSnapshot = databases.Map; - for (var dbId = 0; dbId < databasesMapSize; dbId++) + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + + for (var i = 0; i < activeDbIdsSize; i++) { - if (databasesMapSnapshot[dbId].IsDefault()) continue; + var dbId = activeDbIdsSnapshot[i]; + aofProcessor.Recover(dbId, dbId == 0 ? untilAddress : -1); + + var lastSave = DateTimeOffset.UtcNow; + databasesMapSnapshot[dbId].LastSaveTime = lastSave; + if (dbId == 0) + { replicationOffset = aofProcessor.ReplicationOffset; + this.lastSaveTime = lastSave; + } } aofProcessor.Dispose(); - lastSaveTime = DateTimeOffset.UtcNow; } catch (Exception ex) { @@ -519,6 +530,7 @@ public long ReplayAOF(long untilAddress = -1) if (serverOptions.FailOnRecoveryError) throw; } + return replicationOffset; } @@ -526,32 +538,57 @@ async Task AutoCheckpointBasedOnAofSizeLimit(long aofSizeLimit, CancellationToke { try { + int[] dbIdsToCheckpoint = null; + while (true) { - await Task.Delay(1000); + await Task.Delay(1000, token); if (token.IsCancellationRequested) break; var aofSizeAtLimit = -1l; var databasesMapSize = databases.ActualSize; + + if (!allowMultiDb || databasesMapSize == 1) + { + var dbAofSize = appendOnlyFile.TailAddress - appendOnlyFile.BeginAddress; + if (dbAofSize > aofSizeLimit) + aofSizeAtLimit = dbAofSize; + + if (aofSizeAtLimit != -1) + { + logger?.LogInformation("Enforcing AOF size limit currentAofSize: {currAofSize} > AofSizeLimit: {AofSizeLimit}", aofSizeAtLimit, aofSizeLimit); + await CheckpointTask(StoreType.All, logger: logger); + } + + return; + } + var databasesMapSnapshot = databases.Map; + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; - for (var i = 0; i < databasesMapSize; i++) + if (dbIdsToCheckpoint == null || dbIdsToCheckpoint.Length < databasesMapSize) + dbIdsToCheckpoint = new int[activeDbIdsSize]; + + var dbIdsIdx = 0; + for (var i = 0; i < activeDbIdsSize; i++) { - var db = databasesMapSnapshot[i]; - if (db.IsDefault()) continue; + var dbId = activeDbIdsSnapshot[i]; + var db = databasesMapSnapshot[dbId]; + Debug.Assert(!db.IsDefault()); var dbAofSize = db.AppendOnlyFile.TailAddress - db.AppendOnlyFile.BeginAddress; if (dbAofSize > aofSizeLimit) { - aofSizeAtLimit = dbAofSize; + dbIdsToCheckpoint[dbIdsIdx++] = dbId; break; } } - if (aofSizeAtLimit != -1) + if (dbIdsIdx > 0) { logger?.LogInformation("Enforcing AOF size limit currentAofSize: {currAofSize} > AofSizeLimit: {AofSizeLimit}", aofSizeAtLimit, aofSizeLimit); - TakeCheckpoint(false, logger: logger); + TakeCheckpoint(false, ref dbIdsToCheckpoint, ref checkpointTasks, logger: logger, token: token); } } } @@ -565,9 +602,6 @@ async Task CommitTask(int commitFrequencyMs, ILogger logger = null, Cancellation { try { - int[] activeDbIds = null; - Task[] tasks = null; - while (true) { if (token.IsCancellationRequested) break; @@ -587,7 +621,7 @@ async Task CommitTask(int commitFrequencyMs, ILogger logger = null, Cancellation } else { - MultiDatabaseCommit(ref activeDbIds, ref tasks, token); + MultiDatabaseCommit(ref aofTasks, token); } await Task.Delay(commitFrequencyMs, token); @@ -600,36 +634,32 @@ async Task CommitTask(int commitFrequencyMs, ILogger logger = null, Cancellation } } - void MultiDatabaseCommit(ref int[] activeDbIds, ref Task[] tasks, CancellationToken token) + void MultiDatabaseCommit(ref Task[] tasks, CancellationToken token) { - var databasesMapSize = databases.ActualSize; var databasesMapSnapshot = databases.Map; - if (activeDbIds == null || activeDbIds.Length < databasesMapSize) - { - activeDbIds = new int[databasesMapSize]; - tasks = new Task[databasesMapSize]; - } - var dbIdsIdx = 0; - for (var dbId = 0; dbId < databasesMapSize; dbId++) + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + + tasks ??= new Task[activeDbIdsSize]; + + for (var i = 0; i < activeDbIdsSize; i++) { + var dbId = activeDbIdsSnapshot[i]; var db = databasesMapSnapshot[dbId]; - if (db.IsDefault()) continue; - - tasks[dbIdsIdx] = db.AppendOnlyFile.CommitAsync(null, token).AsTask(); - activeDbIds[dbIdsIdx] = dbId; + Debug.Assert(!db.IsDefault()); - dbIdsIdx++; + tasks[i] = db.AppendOnlyFile.CommitAsync(null, token).AsTask(); } - for (var i = 0; i < dbIdsIdx; i++) + for (var i = 0; i < activeDbIdsSize; i++) { tasks[i].Wait(token); tasks[i] = null; } - if (aofDatabaseIdsPath != null && dbIdsIdx != 0) - WriteDatabaseIdsSnapshot(activeDbIds, 1, dbIdsIdx - 1, aofDatabaseIdsPath); + if (aofDatabaseIdsPath != null) + WriteDatabaseIdsSnapshot(aofDatabaseIdsPath); } async Task CompactionTask(int compactionFrequencySecs, CancellationToken token = default) @@ -641,13 +671,16 @@ async Task CompactionTask(int compactionFrequencySecs, CancellationToken token = { if (token.IsCancellationRequested) return; - var databasesMapSize = databases.ActualSize; var databasesMapSnapshot = databases.Map; - for (var i = 0; i < databasesMapSize; i++) + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + + for (var i = 0; i < activeDbIdsSize; i++) { - var db = databasesMapSnapshot[i]; - if (db.IsDefault()) continue; + var dbId = activeDbIdsSnapshot[i]; + var db = databasesMapSnapshot[dbId]; + Debug.Assert(!db.IsDefault()); DoCompaction(ref db, serverOptions.CompactionMaxSegments, serverOptions.ObjectStoreCompactionMaxSegments, 1, serverOptions.CompactionType, serverOptions.CompactionForceDelete); } @@ -855,10 +888,9 @@ internal void CommitAOF(bool spinWait) return; } - int[] activeDbIds = null; Task[] tasks = null; - MultiDatabaseCommit(ref activeDbIds, ref tasks, CancellationToken.None); + MultiDatabaseCommit(ref tasks, CancellationToken.None); } internal void Start() @@ -901,20 +933,10 @@ private async void IndexAutoGrowTask(CancellationToken token) { try { + var databaseDataInitialized = new bool[serverOptions.MaxDatabases]; var databaseMainStoreIndexMaxedOut = new bool[serverOptions.MaxDatabases]; var databaseObjectStoreIndexMaxedOut = new bool[serverOptions.MaxDatabases]; - var databasesMapSize = databases.ActualSize; - var databasesMapSnapshot = databases.Map; - - for (var i = 0; i < databasesMapSize; i++) - { - if (databasesMapSnapshot[i].IsDefault()) continue; - - databaseMainStoreIndexMaxedOut[i] = serverOptions.AdjustedIndexMaxCacheLines == 0; - databaseObjectStoreIndexMaxedOut[i] = serverOptions.AdjustedObjectStoreIndexMaxCacheLines == 0; - } - var allIndexesMaxedOut = false; while (!allIndexesMaxedOut) @@ -925,31 +947,42 @@ private async void IndexAutoGrowTask(CancellationToken token) await Task.Delay(TimeSpan.FromSeconds(serverOptions.IndexResizeFrequencySecs), token); - databasesMapSize = databases.ActualSize; - databasesMapSnapshot = databases.Map; + var databasesMapSnapshot = databases.Map; - for (var i = 0; i < databasesMapSize; i++) + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + + for (var i = 0; i < activeDbIdsSize; i++) { - if (!databaseMainStoreIndexMaxedOut[i]) + var dbId = activeDbIdsSnapshot[i]; + if (!databaseDataInitialized[dbId]) { - var dbMainStore = databasesMapSnapshot[i].MainStore; - databaseMainStoreIndexMaxedOut[i] = GrowIndexIfNeeded(StoreType.Main, + Debug.Assert(!databasesMapSnapshot[dbId].IsDefault()); + databaseMainStoreIndexMaxedOut[dbId] = serverOptions.AdjustedIndexMaxCacheLines == 0; + databaseObjectStoreIndexMaxedOut[dbId] = serverOptions.AdjustedObjectStoreIndexMaxCacheLines == 0; + databaseDataInitialized[dbId] = true; + } + + if (!databaseMainStoreIndexMaxedOut[dbId]) + { + var dbMainStore = databasesMapSnapshot[dbId].MainStore; + databaseMainStoreIndexMaxedOut[dbId] = GrowIndexIfNeeded(StoreType.Main, serverOptions.AdjustedIndexMaxCacheLines, dbMainStore.OverflowBucketAllocations, () => dbMainStore.IndexSize, () => dbMainStore.GrowIndex()); - if (!databaseMainStoreIndexMaxedOut[i]) + if (!databaseMainStoreIndexMaxedOut[dbId]) allIndexesMaxedOut = false; } - if (!databaseObjectStoreIndexMaxedOut[i]) + if (!databaseObjectStoreIndexMaxedOut[dbId]) { - var dbObjectStore = databasesMapSnapshot[i].ObjectStore; - databaseObjectStoreIndexMaxedOut[i] = GrowIndexIfNeeded(StoreType.Object, + var dbObjectStore = databasesMapSnapshot[dbId].ObjectStore; + databaseObjectStoreIndexMaxedOut[dbId] = GrowIndexIfNeeded(StoreType.Object, serverOptions.AdjustedObjectStoreIndexMaxCacheLines, dbObjectStore.OverflowBucketAllocations, () => dbObjectStore.IndexSize, () => dbObjectStore.GrowIndex()); - if (!databaseObjectStoreIndexMaxedOut[i]) + if (!databaseObjectStoreIndexMaxedOut[dbId]) allIndexesMaxedOut = false; } } @@ -1030,89 +1063,181 @@ public void ResumeCheckpoints() /// Take a checkpoint if no checkpoint was taken after the provided time offset /// /// + /// /// - public async Task TakeOnDemandCheckpoint(DateTimeOffset entryTime) + public async Task TakeOnDemandCheckpoint(DateTimeOffset entryTime, int dbId = 0) { // Take lock to ensure no other task will be taking a checkpoint while (!TryPauseCheckpoints()) await Task.Yield(); // If an external task has taken a checkpoint beyond the provided entryTime return - if (this.lastSaveTime > entryTime) + if (databases.Map[dbId].LastSaveTime > entryTime) { ResumeCheckpoints(); return; } // Necessary to take a checkpoint because the latest checkpoint is before entryTime - await CheckpointTask(StoreType.All, logger: logger); + await CheckpointTask(StoreType.All, dbId, logger: logger); } /// /// Take checkpoint /// /// + /// /// /// + /// + /// /// - public bool TakeCheckpoint(bool background, StoreType storeType = StoreType.All, ILogger logger = null) + public bool TakeCheckpoint(bool background, ref int[] dbIds, ref Task[] tasks, StoreType storeType = StoreType.All, ILogger logger = null, CancellationToken token = default) { // Prevent parallel checkpoint if (!TryPauseCheckpoints()) return false; - if (background) - Task.Run(async () => await CheckpointTask(storeType, logger)); - else - CheckpointTask(storeType, logger).ConfigureAwait(false).GetAwaiter().GetResult(); + + MultiDatabaseCheckpoint(storeType, ref dbIds, ref tasks, background, logger, token); return true; } - private async Task CheckpointTask(StoreType storeType, ILogger logger = null) + /// + /// Take checkpoint of all active databases + /// + /// + /// + /// + /// + /// + public bool TakeCheckpoint(bool background, StoreType storeType = StoreType.All, ILogger logger = null, CancellationToken token = default) + { + var aofSizeAtLimit = -1l; + var databasesMapSize = databases.ActualSize; + + if (!allowMultiDb || databasesMapSize == 1) + { + var checkpointTask = Task.Run(async () => await CheckpointTask(StoreType.All, logger: logger), token); + if (background) + return true; + + checkpointTask.Wait(token); + return true; + } + + var databasesMapSnapshot = databases.Map; + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + + var tasks = new Task[activeDbIdsSize]; + + var dbIdsIdx = 0; + for (var i = 0; i < activeDbIdsSize; i++) + { + var dbId = activeDbIdsSnapshot[i]; + var db = databasesMapSnapshot[dbId]; + Debug.Assert(!db.IsDefault()); + + dbIdsIdx++; + } + + if (dbIdsIdx > 0) + { + return TakeCheckpoint(false, ref activeDbIds, ref tasks, logger: logger, token: token); + } + + return true; + } + + private async Task CheckpointDatabaseTask(int dbId, StoreType storeType, ILogger logger) + { + var databasesMapSnapshot = databases.Map; + var db = databasesMapSnapshot[dbId]; + if (db.IsDefault()) return; + + DoCompaction(ref db); + var lastSaveStoreTailAddress = db.MainStore.Log.TailAddress; + var lastSaveObjectStoreTailAddress = (db.ObjectStore?.Log.TailAddress).GetValueOrDefault(); + + var full = db.LastSaveStoreTailAddress == 0 || + lastSaveStoreTailAddress - db.LastSaveStoreTailAddress >= serverOptions.FullCheckpointLogInterval || + (db.ObjectStore != null && (db.LastSaveObjectStoreTailAddress == 0 || + lastSaveObjectStoreTailAddress - db.LastSaveObjectStoreTailAddress >= serverOptions.FullCheckpointLogInterval)); + + var tryIncremental = serverOptions.EnableIncrementalSnapshots; + if (db.MainStore.IncrementalSnapshotTailAddress >= serverOptions.IncrementalSnapshotLogSizeLimit) + tryIncremental = false; + if (db.ObjectStore?.IncrementalSnapshotTailAddress >= serverOptions.IncrementalSnapshotLogSizeLimit) + tryIncremental = false; + + var checkpointType = serverOptions.UseFoldOverCheckpoints ? CheckpointType.FoldOver : CheckpointType.Snapshot; + await InitiateCheckpoint(dbId, db, full, checkpointType, tryIncremental, storeType, logger); + if (full) + { + if (storeType is StoreType.Main or StoreType.All) + db.LastSaveStoreTailAddress = lastSaveStoreTailAddress; + if (storeType is StoreType.Object or StoreType.All) + db.LastSaveObjectStoreTailAddress = lastSaveObjectStoreTailAddress; + } + + var lastSave = DateTimeOffset.UtcNow; + if (dbId == 0) + lastSaveTime = lastSave; + db.LastSaveTime = lastSave; + } + + private async Task CheckpointTask(StoreType storeType, int dbId = 0, ILogger logger = null) { try { - var databasesMapSize = databases.ActualSize; - var databasesMapSnapshot = databases.Map; + await CheckpointDatabaseTask(dbId, storeType, logger); - var activeDbIds = allowMultiDb ? new List() : null; + if (checkpointDatabaseIdsPath != null) + WriteDatabaseIdsSnapshot(checkpointDatabaseIdsPath); + } + catch (Exception ex) + { + logger?.LogError(ex, "Checkpointing threw exception"); + } + finally + { + ResumeCheckpoints(); + } + } - for (var dbId = 0; dbId < databasesMapSize; dbId++) + private void MultiDatabaseCheckpoint(StoreType storeType, ref int[] dbIds, ref Task[] tasks, bool background, ILogger logger = null, CancellationToken token = default) + { + try + { + Debug.Assert(tasks != null); + + var currIdx = 0; + if (dbIds == null) { - var db = databasesMapSnapshot[dbId]; - if (db.IsDefault()) continue; - - DoCompaction(ref db); - var lastSaveStoreTailAddress = db.MainStore.Log.TailAddress; - var lastSaveObjectStoreTailAddress = (db.ObjectStore?.Log.TailAddress).GetValueOrDefault(); - - var full = db.LastSaveStoreTailAddress == 0 || - lastSaveStoreTailAddress - db.LastSaveStoreTailAddress >= serverOptions.FullCheckpointLogInterval || - (db.ObjectStore != null && (db.LastSaveObjectStoreTailAddress == 0 || - lastSaveObjectStoreTailAddress - db.LastSaveObjectStoreTailAddress >= serverOptions.FullCheckpointLogInterval)); - - var tryIncremental = serverOptions.EnableIncrementalSnapshots; - if (db.MainStore.IncrementalSnapshotTailAddress >= serverOptions.IncrementalSnapshotLogSizeLimit) - tryIncremental = false; - if (db.ObjectStore?.IncrementalSnapshotTailAddress >= serverOptions.IncrementalSnapshotLogSizeLimit) - tryIncremental = false; - - var checkpointType = serverOptions.UseFoldOverCheckpoints ? CheckpointType.FoldOver : CheckpointType.Snapshot; - await InitiateCheckpoint(dbId, db, full, checkpointType, tryIncremental, storeType, logger); - if (full) + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + while(currIdx < activeDbIdsSize) { - if (storeType is StoreType.Main or StoreType.All) - db.LastSaveStoreTailAddress = lastSaveStoreTailAddress; - if (storeType is StoreType.Object or StoreType.All) - db.LastSaveObjectStoreTailAddress = lastSaveObjectStoreTailAddress; + var dbId = activeDbIdsSnapshot[currIdx]; + tasks[currIdx] = CheckpointDatabaseTask(dbId, storeType, logger); + currIdx++; } - - if(allowMultiDb && dbId != 0) - activeDbIds!.Add(dbId); } + else + { + while (currIdx < dbIds.Length && (currIdx == 0 || dbIds[currIdx] != 0)) + { + tasks[currIdx] = CheckpointDatabaseTask(dbIds[currIdx], storeType, logger); + currIdx++; + } + } + + if (background) return; - if (allowMultiDb && checkpointDatabaseIdsPath != null && activeDbIds!.Count > 0) - WriteDatabaseIdsSnapshot(activeDbIds.ToArray(), 0, activeDbIds.Count, checkpointDatabaseIdsPath); + for (var i = 0; i < currIdx; i++) + tasks[i].Wait(token); - lastSaveTime = DateTimeOffset.UtcNow; + if (checkpointDatabaseIdsPath != null) + WriteDatabaseIdsSnapshot(checkpointDatabaseIdsPath); } catch (Exception ex) { @@ -1246,6 +1371,54 @@ public void GetDatabaseStores(int dbId, } } + private void HandleDatabaseAdded(int dbId) + { + lock (activeDbIdsLock) + { + databaseCount++; + + if (activeDbIds != null && databaseCount < activeDbIds.Length) + { + activeDbIds[databaseCount - 1] = dbId; + activeDbIdsLength++; + return; + } + + var newSize = activeDbIds?.Length ?? 1; + while (databaseCount >= newSize) + { + newSize = Math.Min(this.serverOptions.MaxDatabases, newSize * 2); + } + + var activeDbIdsUpdated = new int[newSize]; + var databasesMapSize = databases.ActualSize; + var databasesMapSnapshot = databases.Map; + + var activeDbIdsIdx = 0; + for (var i = 0; i < databasesMapSize; i++) + { + var currDb = databasesMapSnapshot[i]; + if (currDb.IsDefault()) + continue; + activeDbIdsUpdated[activeDbIdsIdx++] = i; + } + + checkpointTasks = new Task[newSize]; + aofTasks = new Task[newSize]; + activeDbIds = activeDbIdsUpdated; + activeDbIdsLength = activeDbIdsIdx; + } + } + + public bool TrySetDatabase(int dbId, ref GarnetDatabase db) + { + if (!allowMultiDb || !databases.TrySetValue(dbId, ref db)) + return false; + + HandleDatabaseAdded(dbId); + return true; + } + public bool TryGetOrSetDatabase(int dbId, out GarnetDatabase db) { db = default; @@ -1254,7 +1427,7 @@ public bool TryGetOrSetDatabase(int dbId, out GarnetDatabase db) return false; if (added) - Interlocked.Increment(ref databaseCount); + HandleDatabaseAdded(dbId); return true; } From 547dd949b112e4de1669c943b7a44c8a9748f2bc Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Wed, 5 Feb 2025 13:40:35 -0800 Subject: [PATCH 13/82] wip --- libs/server/StoreWrapper.cs | 48 ++++++++++++------------------------- 1 file changed, 15 insertions(+), 33 deletions(-) diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 58c9f89bc8..5dad8195e4 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -588,7 +588,7 @@ async Task AutoCheckpointBasedOnAofSizeLimit(long aofSizeLimit, CancellationToke if (dbIdsIdx > 0) { logger?.LogInformation("Enforcing AOF size limit currentAofSize: {currAofSize} > AofSizeLimit: {AofSizeLimit}", aofSizeAtLimit, aofSizeLimit); - TakeCheckpoint(false, ref dbIdsToCheckpoint, ref checkpointTasks, logger: logger, token: token); + TakeCheckpoint( ref dbIdsToCheckpoint, ref checkpointTasks, logger: logger, token: token); } } } @@ -634,7 +634,7 @@ async Task CommitTask(int commitFrequencyMs, ILogger logger = null, Cancellation } } - void MultiDatabaseCommit(ref Task[] tasks, CancellationToken token) + void MultiDatabaseCommit(ref Task[] tasks, CancellationToken token, bool spinWait = true) { var databasesMapSnapshot = databases.Map; @@ -652,14 +652,16 @@ void MultiDatabaseCommit(ref Task[] tasks, CancellationToken token) tasks[i] = db.AppendOnlyFile.CommitAsync(null, token).AsTask(); } - for (var i = 0; i < activeDbIdsSize; i++) + var completion = Task.WhenAll(tasks).ContinueWith(_ => { - tasks[i].Wait(token); - tasks[i] = null; - } + if (aofDatabaseIdsPath != null) + WriteDatabaseIdsSnapshot(aofDatabaseIdsPath); + }, token); - if (aofDatabaseIdsPath != null) - WriteDatabaseIdsSnapshot(aofDatabaseIdsPath); + if (!spinWait) + return; + + completion.Wait(token); } async Task CompactionTask(int compactionFrequencySecs, CancellationToken token = default) @@ -890,7 +892,7 @@ internal void CommitAOF(bool spinWait) Task[] tasks = null; - MultiDatabaseCommit(ref tasks, CancellationToken.None); + MultiDatabaseCommit(ref tasks, CancellationToken.None, spinWait); } internal void Start() @@ -1092,12 +1094,12 @@ public async Task TakeOnDemandCheckpoint(DateTimeOffset entryTime, int dbId = 0) /// /// /// - public bool TakeCheckpoint(bool background, ref int[] dbIds, ref Task[] tasks, StoreType storeType = StoreType.All, ILogger logger = null, CancellationToken token = default) + public bool TakeCheckpoint(ref int[] dbIds, ref Task[] tasks, StoreType storeType = StoreType.All, ILogger logger = null, CancellationToken token = default) { // Prevent parallel checkpoint if (!TryPauseCheckpoints()) return false; - MultiDatabaseCheckpoint(storeType, ref dbIds, ref tasks, background, logger, token); + MultiDatabaseCheckpoint(storeType, ref dbIds, ref tasks, logger, token); return true; } @@ -1111,7 +1113,6 @@ public bool TakeCheckpoint(bool background, ref int[] dbIds, ref Task[] tasks, S /// public bool TakeCheckpoint(bool background, StoreType storeType = StoreType.All, ILogger logger = null, CancellationToken token = default) { - var aofSizeAtLimit = -1l; var databasesMapSize = databases.ActualSize; if (!allowMultiDb || databasesMapSize == 1) @@ -1124,28 +1125,11 @@ public bool TakeCheckpoint(bool background, StoreType storeType = StoreType.All, return true; } - var databasesMapSnapshot = databases.Map; var activeDbIdsSize = activeDbIdsLength; var activeDbIdsSnapshot = activeDbIds; var tasks = new Task[activeDbIdsSize]; - - var dbIdsIdx = 0; - for (var i = 0; i < activeDbIdsSize; i++) - { - var dbId = activeDbIdsSnapshot[i]; - var db = databasesMapSnapshot[dbId]; - Debug.Assert(!db.IsDefault()); - - dbIdsIdx++; - } - - if (dbIdsIdx > 0) - { - return TakeCheckpoint(false, ref activeDbIds, ref tasks, logger: logger, token: token); - } - - return true; + return TakeCheckpoint(ref activeDbIdsSnapshot, ref tasks, logger: logger, token: token); } private async Task CheckpointDatabaseTask(int dbId, StoreType storeType, ILogger logger) @@ -1204,7 +1188,7 @@ private async Task CheckpointTask(StoreType storeType, int dbId = 0, ILogger log } } - private void MultiDatabaseCheckpoint(StoreType storeType, ref int[] dbIds, ref Task[] tasks, bool background, ILogger logger = null, CancellationToken token = default) + private void MultiDatabaseCheckpoint(StoreType storeType, ref int[] dbIds, ref Task[] tasks, ILogger logger = null, CancellationToken token = default) { try { @@ -1231,8 +1215,6 @@ private void MultiDatabaseCheckpoint(StoreType storeType, ref int[] dbIds, ref T } } - if (background) return; - for (var i = 0; i < currIdx; i++) tasks[i].Wait(token); From d1fae464bcfbf9cb3ac5dfd30456c9e9c7197af5 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Wed, 5 Feb 2025 16:54:40 -0800 Subject: [PATCH 14/82] wip --- .../Server/Migration/MigrateSessionKeys.cs | 4 +- .../Session/RespClusterMigrateCommands.cs | 2 +- libs/resources/RespCommandsInfo.json | 2 +- libs/server/AOF/AofProcessor.cs | 4 +- libs/server/GarnetDatabase.cs | 14 +- libs/server/Resp/AdminCommands.cs | 15 +- libs/server/Resp/RespServerSession.cs | 4 +- libs/server/Servers/StoreApi.cs | 7 +- libs/server/StoreWrapper.cs | 205 ++++++++++++++---- .../GarnetCommandsDocs.json | 17 ++ .../GarnetCommandsInfo.json | 7 + 11 files changed, 219 insertions(+), 62 deletions(-) diff --git a/libs/cluster/Server/Migration/MigrateSessionKeys.cs b/libs/cluster/Server/Migration/MigrateSessionKeys.cs index 61942ec69c..04682c81a2 100644 --- a/libs/cluster/Server/Migration/MigrateSessionKeys.cs +++ b/libs/cluster/Server/Migration/MigrateSessionKeys.cs @@ -183,14 +183,14 @@ public bool MigrateKeys() return false; // Migrate main store keys - _gcs.InitMigrateBuffer(clusterProvider.storeWrapper.loggingFrequncy); + _gcs.InitMigrateBuffer(clusterProvider.storeWrapper.loggingFrequency); if (!MigrateKeysFromMainStore()) return false; // Migrate object store keys if (!clusterProvider.serverOptions.DisableObjects) { - _gcs.InitMigrateBuffer(clusterProvider.storeWrapper.loggingFrequncy); + _gcs.InitMigrateBuffer(clusterProvider.storeWrapper.loggingFrequency); if (!MigrateKeysFromObjectStore()) return false; } diff --git a/libs/cluster/Session/RespClusterMigrateCommands.cs b/libs/cluster/Session/RespClusterMigrateCommands.cs index b50a23cb29..c8f77b971b 100644 --- a/libs/cluster/Session/RespClusterMigrateCommands.cs +++ b/libs/cluster/Session/RespClusterMigrateCommands.cs @@ -26,7 +26,7 @@ private void TrackImportProgress(int keyCount, bool isMainStore, bool completed { totalKeyCount += keyCount; var duration = TimeSpan.FromTicks(Stopwatch.GetTimestamp() - lastLog); - if (completed || lastLog == 0 || duration >= clusterProvider.storeWrapper.loggingFrequncy) + if (completed || lastLog == 0 || duration >= clusterProvider.storeWrapper.loggingFrequency) { logger?.LogTrace("[{op}]: isMainStore:({storeType}) totalKeyCount:({totalKeyCount})", completed ? "COMPLETED" : "IMPORTING", isMainStore, totalKeyCount.ToString("N0")); lastLog = Stopwatch.GetTimestamp(); diff --git a/libs/resources/RespCommandsInfo.json b/libs/resources/RespCommandsInfo.json index 06a5b26ffd..4015ef5a69 100644 --- a/libs/resources/RespCommandsInfo.json +++ b/libs/resources/RespCommandsInfo.json @@ -3683,7 +3683,7 @@ { "Command": "SAVE", "Name": "SAVE", - "Arity": 1, + "Arity": -1, "Flags": "Admin, NoAsyncLoading, NoMulti, NoScript", "AclCategories": "Admin, Dangerous, Slow" }, diff --git a/libs/server/AOF/AofProcessor.cs b/libs/server/AOF/AofProcessor.cs index f8d683944a..6989aeca7a 100644 --- a/libs/server/AOF/AofProcessor.cs +++ b/libs/server/AOF/AofProcessor.cs @@ -201,14 +201,14 @@ public unsafe void ProcessAofRecordInternal(byte* ptr, int length, bool asReplic if (asReplica) { if (header.storeVersion > storeWrapper.store.CurrentVersion) - storeWrapper.TakeCheckpoint(false, StoreType.Main, logger); + storeWrapper.TakeCheckpoint(false, StoreType.Main, logger: logger); } break; case AofEntryType.ObjectStoreCheckpointCommit: if (asReplica) { if (header.storeVersion > storeWrapper.objectStore.CurrentVersion) - storeWrapper.TakeCheckpoint(false, StoreType.Object, logger); + storeWrapper.TakeCheckpoint(false, StoreType.Object, logger: logger); } break; default: diff --git a/libs/server/GarnetDatabase.cs b/libs/server/GarnetDatabase.cs index b777797c73..6d5ecb5c8c 100644 --- a/libs/server/GarnetDatabase.cs +++ b/libs/server/GarnetDatabase.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using Tsavorite.core; namespace Garnet.server @@ -33,7 +34,7 @@ public struct GarnetDatabase : IDisposable /// /// Size Tracker for Object Store /// - public CacheSizeTracker ObjectSizeTracker; + public CacheSizeTracker ObjectStoreSizeTracker; /// /// Device used for AOF logging @@ -69,16 +70,17 @@ public struct GarnetDatabase : IDisposable public GarnetDatabase(TsavoriteKV mainStore, TsavoriteKV objectStore, - CacheSizeTracker objectSizeTracker, IDevice aofDevice, TsavoriteLog appendOnlyFile) + CacheSizeTracker objectStoreSizeTracker, IDevice aofDevice, TsavoriteLog appendOnlyFile) { MainStore = mainStore; ObjectStore = objectStore; - ObjectSizeTracker = objectSizeTracker; + ObjectStoreSizeTracker = objectStoreSizeTracker; AofDevice = aofDevice; AppendOnlyFile = appendOnlyFile; VersionMap = new WatchVersionMap(DefaultVersionMapSize); LastSaveStoreTailAddress = 0; LastSaveObjectStoreTailAddress = 0; + LastSaveTime = DateTimeOffset.FromUnixTimeSeconds(0); } public bool IsDefault() => MainStore == null; @@ -92,6 +94,12 @@ public void Dispose() AofDevice?.Dispose(); AppendOnlyFile?.Dispose(); + if (ObjectStoreSizeTracker != null) + { + while (!ObjectStoreSizeTracker.Stopped) + Thread.Yield(); + } + disposed = true; } } diff --git a/libs/server/Resp/AdminCommands.cs b/libs/server/Resp/AdminCommands.cs index 7a2e6fd000..b66cd7c4f5 100644 --- a/libs/server/Resp/AdminCommands.cs +++ b/libs/server/Resp/AdminCommands.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using System; -using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -156,7 +155,7 @@ static void OnACLOrNoScriptFailure(RespServerSession self, RespCommand cmd) void CommitAof() { - storeWrapper.appendOnlyFile?.CommitAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + storeWrapper.CommitAOFAsync().ConfigureAwait(false).GetAwaiter().GetResult(); } private bool NetworkMonitor() @@ -649,12 +648,18 @@ private bool NetworkProcessClusterCommand(RespCommand command) private bool NetworkSAVE() { - if (parseState.Count != 0) + if (parseState.Count > 1) { return AbortWithWrongNumberOfArguments(nameof(RespCommand.SAVE)); } - if (!storeWrapper.TakeCheckpoint(false, StoreType.All, logger)) + var dbId = -1; + if (parseState.Count == 1) + { + parseState.TryGetInt(0, out dbId); + } + + if (!storeWrapper.TakeCheckpoint(false, dbId: dbId, logger: logger)) { while (!RespWriteUtils.TryWriteError("ERR checkpoint already in progress"u8, ref dcurr, dend)) SendAndReset(); @@ -689,7 +694,7 @@ private bool NetworkBGSAVE() return AbortWithWrongNumberOfArguments(nameof(RespCommand.BGSAVE)); } - var success = storeWrapper.TakeCheckpoint(true, StoreType.All, logger); + var success = storeWrapper.TakeCheckpoint(true, StoreType.All, logger: logger); if (success) { while (!RespWriteUtils.TryWriteSimpleString("Background saving started"u8, ref dcurr, dend)) diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index 7bd79d583c..348a606129 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -1169,7 +1169,7 @@ private void Send(byte* d) // Debug.WriteLine("SEND: [" + Encoding.UTF8.GetString(new Span(d, (int)(dcurr - d))).Replace("\n", "|").Replace("\r", "!") + "]"); if (waitForAofBlocking) { - var task = storeWrapper.appendOnlyFile.WaitForCommitAsync(); + var task = storeWrapper.WaitForCommitAsync(); if (!task.IsCompleted) task.AsTask().GetAwaiter().GetResult(); } int sendBytes = (int)(dcurr - d); @@ -1189,7 +1189,7 @@ private void DebugSend(byte* d) { if (storeWrapper.appendOnlyFile != null && storeWrapper.serverOptions.WaitForCommit) { - var task = storeWrapper.appendOnlyFile.WaitForCommitAsync(); + var task = storeWrapper.WaitForCommitAsync(); if (!task.IsCompleted) task.AsTask().GetAwaiter().GetResult(); } int sendBytes = (int)(dcurr - d); diff --git a/libs/server/Servers/StoreApi.cs b/libs/server/Servers/StoreApi.cs index 5b20cc5ef0..e6f67b0c1a 100644 --- a/libs/server/Servers/StoreApi.cs +++ b/libs/server/Servers/StoreApi.cs @@ -3,7 +3,6 @@ using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; namespace Garnet.server { @@ -32,17 +31,17 @@ public StoreApi(StoreWrapper storeWrapper) /// /// Wait for commit /// - public ValueTask WaitForCommitAsync(CancellationToken token = default) => storeWrapper.appendOnlyFile != null ? storeWrapper.appendOnlyFile.WaitForCommitAsync(token: token) : ValueTask.CompletedTask; + public ValueTask WaitForCommitAsync(CancellationToken token = default) => storeWrapper.WaitForCommitAsync(token: token); /// /// Wait for commit /// - public void WaitForCommit() => storeWrapper.appendOnlyFile?.WaitForCommit(); + public void WaitForCommit() => storeWrapper.WaitForCommit(); /// /// Commit AOF /// - public ValueTask CommitAOFAsync(CancellationToken token) => storeWrapper.appendOnlyFile != null ? storeWrapper.appendOnlyFile.CommitAsync(null, token) : ValueTask.CompletedTask; + public ValueTask CommitAOFAsync(CancellationToken token) => storeWrapper.CommitAOFAsync(token); /// /// Flush DB (delete all keys) diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 5dad8195e4..319aea1138 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -104,7 +104,7 @@ public sealed class StoreWrapper /// public readonly ConcurrentDictionary storeScriptCache; - public readonly TimeSpan loggingFrequncy; + public readonly TimeSpan loggingFrequency; /// /// Number of current logical databases @@ -122,12 +122,12 @@ public sealed class StoreWrapper int activeDbIdsLength; Task[] checkpointTasks = null; Task[] aofTasks = null; - object activeDbIdsLock = new(); + readonly object activeDbIdsLock = new(); - string databaseIdsFileName = "dbIds.dat"; - string aofDatabaseIdsPath; - string checkpointDatabaseIdsPath; - IStreamProvider databaseIdsStreamProvider; + const string DatabaseIdsFileName = "dbIds.dat"; + readonly string aofDatabaseIdsPath; + readonly string checkpointDatabaseIdsPath; + readonly IStreamProvider databaseIdsStreamProvider; public delegate GarnetDatabase DatabaseCreatorDelegate(int dbId, out string storeCheckpointDir, out string aofDir); @@ -163,7 +163,7 @@ public StoreWrapper( this.sessionLogger = loggerFactory?.CreateLogger("Session"); this.accessControlList = accessControlList; this.GarnetObjectSerializer = new GarnetObjectSerializer(this.customCommandManager); - this.loggingFrequncy = TimeSpan.FromSeconds(serverOptions.LoggingFrequency); + this.loggingFrequency = TimeSpan.FromSeconds(serverOptions.LoggingFrequency); this.allowMultiDb = this.serverOptions.MaxDatabases > 1; @@ -174,8 +174,8 @@ public StoreWrapper( if (createDefaultDatabase) { var db = createDatabasesDelegate(0, out var storeCheckpointDir, out var aofPath); - checkpointDatabaseIdsPath = storeCheckpointDir == null ? null : Path.Combine(storeCheckpointDir, databaseIdsFileName); - aofDatabaseIdsPath = aofPath == null ? null : Path.Combine(aofPath, databaseIdsFileName); + checkpointDatabaseIdsPath = storeCheckpointDir == null ? null : Path.Combine(storeCheckpointDir, DatabaseIdsFileName); + aofDatabaseIdsPath = aofPath == null ? null : Path.Combine(aofPath, DatabaseIdsFileName); if (!this.TrySetDatabase(0, ref db)) throw new GarnetException("Failed to set initial database in databases map"); @@ -274,7 +274,7 @@ private void CopyDatabases(StoreWrapper src, bool recordToAof) for (var dbId = 0; dbId < databasesMapSize; dbId++) { var db = databasesMapSnapshot[dbId]; - var dbCopy = new GarnetDatabase(db.MainStore, db.ObjectStore, db.ObjectSizeTracker, + var dbCopy = new GarnetDatabase(db.MainStore, db.ObjectStore, db.ObjectStoreSizeTracker, recordToAof ? db.AofDevice : null, recordToAof ? db.AppendOnlyFile : null); this.TrySetDatabase(dbId, ref dbCopy); } @@ -287,7 +287,7 @@ private void InitializeFieldsFromDatabase(GarnetDatabase db) // Set fields to default database this.store = db.MainStore; this.objectStore = db.ObjectStore; - this.objectStoreSizeTracker = db.ObjectSizeTracker; + this.objectStoreSizeTracker = db.ObjectStoreSizeTracker; this.appendOnlyFile = db.AppendOnlyFile; this.versionMap = db.VersionMap; } @@ -330,7 +330,7 @@ internal FunctionsState CreateFunctionsState(int dbId = 0) if (!this.TryGetOrSetDatabase(dbId, out var db)) throw new GarnetException($"Database with ID {dbId} was not found."); - return new(db.AppendOnlyFile, db.VersionMap, customCommandManager, null, db.ObjectSizeTracker, + return new(db.AppendOnlyFile, db.VersionMap, customCommandManager, null, db.ObjectStoreSizeTracker, GarnetObjectSerializer); } @@ -376,23 +376,33 @@ public void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStore objectStoreVersion = !recoverObjectStoreFromToken ? objectStore.Recover() : objectStore.Recover(metadata.objectStoreIndexToken, metadata.objectStoreHlogToken); } } + + if (storeVersion > 0 || objectStoreVersion > 0) + lastSaveTime = DateTimeOffset.UtcNow; } else { if (allowMultiDb && checkpointDatabaseIdsPath != null) RecoverDatabases(checkpointDatabaseIdsPath); - var databasesMapSize = databases.ActualSize; var databasesMapSnapshot = databases.Map; - for (var i = 0; i < databasesMapSize; i++) + + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + + for (var i = 0; i < activeDbIdsSize; i++) { - var db = databasesMapSnapshot[i]; + var dbId = activeDbIdsSnapshot[i]; + var db = databasesMapSnapshot[dbId]; storeVersion = db.MainStore.Recover(); if (db.ObjectStore != null) objectStoreVersion = db.ObjectStore.Recover(); + + var lastSave = DateTimeOffset.UtcNow; + db.LastSaveTime = lastSave; + if (dbId == 0 && (storeVersion > 0 || objectStoreVersion > 0)) + lastSaveTime = lastSave; } } - if (storeVersion > 0 || objectStoreVersion > 0) - lastSaveTime = DateTimeOffset.UtcNow; } catch (TsavoriteNoHybridLogException ex) { @@ -469,16 +479,21 @@ public void RecoverAOF() /// /// Reset /// - public void Reset() + public void Reset(int dbId = 0) { try { - if (store.Log.TailAddress > 64) - store.Reset(); - if (objectStore?.Log.TailAddress > 64) - objectStore?.Reset(); - appendOnlyFile?.Reset(); - lastSaveTime = DateTimeOffset.FromUnixTimeSeconds(0); + var db = databases.Map[dbId]; + if (db.MainStore.Log.TailAddress > 64) + db.MainStore.Reset(); + if (db.ObjectStore?.Log.TailAddress > 64) + db.ObjectStore?.Reset(); + db.AppendOnlyFile?.Reset(); + + var lastSave = DateTimeOffset.FromUnixTimeSeconds(0); + if (dbId == 0) + lastSaveTime = lastSave; + db.LastSaveTime = lastSave; } catch (Exception ex) { @@ -757,7 +772,8 @@ void DoCompaction(ref GarnetDatabase db) /// /// /// - public void EnqueueCommit(bool isMainStore, long version) + /// + public void EnqueueCommit(bool isMainStore, long version, int dbId = 0) { AofHeader header = new() { @@ -765,7 +781,9 @@ public void EnqueueCommit(bool isMainStore, long version) storeVersion = version, sessionID = -1 }; - appendOnlyFile?.Enqueue(header, out _); + + var aof = databases.Map[dbId].AppendOnlyFile; + aof?.Enqueue(header, out _); } void DoCompaction(ref GarnetDatabase db, int mainStoreMaxSegments, int objectStoreMaxSegments, int numSegmentsToCompact, LogCompactionType compactionType, bool compactionForceDelete) @@ -793,7 +811,7 @@ void DoCompaction(ref GarnetDatabase db, int mainStoreMaxSegments, int objectSto mainStoreLog.Compact>(new SpanByteFunctions(), untilAddress, CompactionType.Scan); if (compactionForceDelete) { - CompactionCommitAof(); + CompactionCommitAof(ref db); mainStoreLog.Truncate(); } break; @@ -802,7 +820,7 @@ void DoCompaction(ref GarnetDatabase db, int mainStoreMaxSegments, int objectSto mainStoreLog.Compact>(new SpanByteFunctions(), untilAddress, CompactionType.Lookup); if (compactionForceDelete) { - CompactionCommitAof(); + CompactionCommitAof(ref db); mainStoreLog.Truncate(); } break; @@ -825,7 +843,7 @@ void DoCompaction(ref GarnetDatabase db, int mainStoreMaxSegments, int objectSto long readOnlyAddress = objectStoreLog.ReadOnlyAddress; long compactLength = (1L << serverOptions.ObjectStoreSegmentSizeBits()) * (objectStoreMaxSegments - numSegmentsToCompact); long untilAddress = readOnlyAddress - compactLength; - logger?.LogInformation("Begin object store compact until {untilAddress}, Begin = {beginAddress}, ReadOnly = {readOnlyAddress}, Tail = {tailAddress}", untilAddress, objectStore.Log.BeginAddress, readOnlyAddress, objectStore.Log.TailAddress); + logger?.LogInformation("Begin object store compact until {untilAddress}, Begin = {beginAddress}, ReadOnly = {readOnlyAddress}, Tail = {tailAddress}", untilAddress, objectStoreLog.BeginAddress, readOnlyAddress, objectStoreLog.TailAddress); switch (compactionType) { @@ -838,7 +856,7 @@ void DoCompaction(ref GarnetDatabase db, int mainStoreMaxSegments, int objectSto new SimpleSessionFunctions(), untilAddress, CompactionType.Scan); if (compactionForceDelete) { - CompactionCommitAof(); + CompactionCommitAof(ref db); objectStoreLog.Truncate(); } break; @@ -848,7 +866,7 @@ void DoCompaction(ref GarnetDatabase db, int mainStoreMaxSegments, int objectSto new SimpleSessionFunctions(), untilAddress, CompactionType.Lookup); if (compactionForceDelete) { - CompactionCommitAof(); + CompactionCommitAof(ref db); objectStoreLog.Truncate(); } break; @@ -861,7 +879,7 @@ void DoCompaction(ref GarnetDatabase db, int mainStoreMaxSegments, int objectSto } } - void CompactionCommitAof() + void CompactionCommitAof(ref GarnetDatabase db) { // If we are the primary, we commit the AOF. // If we are the replica, we commit the AOF only if fast commit is disabled @@ -872,17 +890,19 @@ void CompactionCommitAof() if (serverOptions.EnableCluster && clusterProvider.IsReplica()) { if (!serverOptions.EnableFastCommit) - appendOnlyFile?.CommitAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + db.AppendOnlyFile?.CommitAsync().ConfigureAwait(false).GetAwaiter().GetResult(); } else { - appendOnlyFile?.CommitAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + db.AppendOnlyFile?.CommitAsync().ConfigureAwait(false).GetAwaiter().GetResult(); } } } internal void CommitAOF(bool spinWait) { + if (!serverOptions.EnableAOF || appendOnlyFile == null) return; + var databasesMapSize = databases.ActualSize; if (!allowMultiDb || databasesMapSize == 1) { @@ -895,6 +915,85 @@ internal void CommitAOF(bool spinWait) MultiDatabaseCommit(ref tasks, CancellationToken.None, spinWait); } + internal void WaitForCommit() + { + if (!serverOptions.EnableAOF || appendOnlyFile == null) return; + + var databasesMapSize = databases.ActualSize; + if (!allowMultiDb || databasesMapSize == 1) + { + appendOnlyFile.WaitForCommit(); + return; + } + + var databasesMapSnapshot = databases.Map; + + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + + var tasks = new Task[activeDbIdsSize]; + for (var i = 0; i < activeDbIdsSize; i++) + { + var dbId = activeDbIdsSnapshot[i]; + var db = databasesMapSnapshot[dbId]; + tasks[i] = db.AppendOnlyFile.WaitForCommitAsync().AsTask(); + } + + Task.WaitAll(tasks); + } + + internal async ValueTask WaitForCommitAsync(CancellationToken token = default) + { + if (!serverOptions.EnableAOF || appendOnlyFile == null) return; + + var databasesMapSize = databases.ActualSize; + if (!allowMultiDb || databasesMapSize == 1) + { + await appendOnlyFile.WaitForCommitAsync(token: token); + } + + var databasesMapSnapshot = databases.Map; + + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + + var tasks = new Task[activeDbIdsSize]; + for (var i = 0; i < activeDbIdsSize; i++) + { + var dbId = activeDbIdsSnapshot[i]; + var db = databasesMapSnapshot[dbId]; + tasks[i] = db.AppendOnlyFile.WaitForCommitAsync(token: token).AsTask(); + } + + await Task.WhenAll(tasks); + } + + internal async ValueTask CommitAOFAsync(CancellationToken token = default) + { + if (!serverOptions.EnableAOF || appendOnlyFile == null) return; + + var databasesMapSize = databases.ActualSize; + if (!allowMultiDb || databasesMapSize == 1) + { + await appendOnlyFile.CommitAsync(token: token); + } + + var databasesMapSnapshot = databases.Map; + + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + + var tasks = new Task[activeDbIdsSize]; + for (var i = 0; i < activeDbIdsSize; i++) + { + var dbId = activeDbIdsSnapshot[i]; + var db = databasesMapSnapshot[dbId]; + tasks[i] = db.AppendOnlyFile.CommitAsync(token: token).AsTask(); + } + + await Task.WhenAll(tasks); + } + internal void Start() { monitor?.Start(); @@ -926,7 +1025,23 @@ internal void Start() Task.Run(() => IndexAutoGrowTask(ctsCommit.Token)); } - objectStoreSizeTracker?.Start(ctsCommit.Token); + var databasesMapSize = databases.ActualSize; + + if (!allowMultiDb || databasesMapSize == 1) + objectStoreSizeTracker?.Start(ctsCommit.Token); + else + { + var databasesMapSnapshot = databases.Map; + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + + for (var i = 0; i < activeDbIdsSize; i++) + { + var dbId = activeDbIdsSnapshot[i]; + var db = databasesMapSnapshot[dbId]; + db.ObjectStoreSizeTracker?.Start(ctsCommit.Token); + } + } } /// Grows indexes of both main store and object store if current size is too small. @@ -1108,16 +1223,17 @@ public bool TakeCheckpoint(ref int[] dbIds, ref Task[] tasks, StoreType storeTyp /// /// /// + /// /// /// /// - public bool TakeCheckpoint(bool background, StoreType storeType = StoreType.All, ILogger logger = null, CancellationToken token = default) + public bool TakeCheckpoint(bool background, StoreType storeType = StoreType.All, int dbId = -1, ILogger logger = null, CancellationToken token = default) { var databasesMapSize = databases.ActualSize; - if (!allowMultiDb || databasesMapSize == 1) + if (!allowMultiDb || databasesMapSize == 1 || dbId != -1) { - var checkpointTask = Task.Run(async () => await CheckpointTask(StoreType.All, logger: logger), token); + var checkpointTask = Task.Run(async () => await CheckpointTask(StoreType.All, dbId, logger: logger), token); if (background) return true; @@ -1294,13 +1410,14 @@ private async Task InitiateCheckpoint(int dbId, GarnetDatabase db, bool full, Ch logger?.LogInformation("Completed checkpoint"); } - public bool HasKeysInSlots(List slots) + public bool HasKeysInSlots(List slots, int dbId = 0) { if (slots.Count > 0) { + GetDatabaseStores(dbId, out var dbMainStore, out var dbObjectStore); bool hasKeyInSlots = false; { - using var iter = store.Iterate>(new SimpleSessionFunctions()); + using var iter = dbMainStore.Iterate>(new SimpleSessionFunctions()); while (!hasKeyInSlots && iter.GetNext(out RecordInfo record)) { ref var key = ref iter.GetKey(); @@ -1312,11 +1429,11 @@ public bool HasKeysInSlots(List slots) } } - if (!hasKeyInSlots && objectStore != null) + if (!hasKeyInSlots && dbObjectStore != null) { var functionsState = CreateFunctionsState(); var objstorefunctions = new ObjectSessionFunctions(functionsState); - var objectStoreSession = objectStore?.NewSession(objstorefunctions); + var objectStoreSession = dbObjectStore?.NewSession(objstorefunctions); var iter = objectStoreSession.Iterate(); while (!hasKeyInSlots && iter.GetNext(out RecordInfo record)) { @@ -1358,6 +1475,10 @@ private void HandleDatabaseAdded(int dbId) lock (activeDbIdsLock) { databaseCount++; + var db = databases.Map[dbId]; + if (dbId != 0 && objectStoreSizeTracker != null && !objectStoreSizeTracker.Stopped && + db.ObjectStoreSizeTracker != null && db.ObjectStoreSizeTracker.Stopped) + db.ObjectStoreSizeTracker?.Start(ctsCommit.Token); if (activeDbIds != null && databaseCount < activeDbIds.Length) { diff --git a/playground/CommandInfoUpdater/GarnetCommandsDocs.json b/playground/CommandInfoUpdater/GarnetCommandsDocs.json index 1599c22037..4b02b59e06 100644 --- a/playground/CommandInfoUpdater/GarnetCommandsDocs.json +++ b/playground/CommandInfoUpdater/GarnetCommandsDocs.json @@ -611,6 +611,23 @@ } ] }, + { + "Command": "SAVE", + "Name": "SAVE", + "Summary": "Synchronously saves the database(s) to disk.", + "Group": "Server", + "Complexity": "O(N) where N is the total number of keys in all databases", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "DBID", + "DisplayText": "dbid", + "Type": "Integer", + "Token": "DBID", + "ArgumentFlags": "Optional" + } + ] + }, { "Command": "SET", "Name": "SET", diff --git a/playground/CommandInfoUpdater/GarnetCommandsInfo.json b/playground/CommandInfoUpdater/GarnetCommandsInfo.json index b7adc23f4c..ab8e66d7b1 100644 --- a/playground/CommandInfoUpdater/GarnetCommandsInfo.json +++ b/playground/CommandInfoUpdater/GarnetCommandsInfo.json @@ -667,6 +667,13 @@ } ] }, + { + "Command": "SAVE", + "Name": "SAVE", + "Arity": -1, + "Flags": "Admin, NoAsyncLoading, NoMulti, NoScript", + "AclCategories": "Admin, Dangerous, Slow" + }, { "Command": "SETIFMATCH", "Name": "SETIFMATCH", From 71efd73341c4f01737315355566650dce3a42154 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Wed, 5 Feb 2025 19:09:02 -0800 Subject: [PATCH 15/82] wip --- libs/server/AOF/AofProcessor.cs | 2 +- libs/server/Metrics/Info/GarnetInfoMetrics.cs | 4 +- .../Storage/SizeTracker/CacheSizeTracker.cs | 5 + libs/server/StoreWrapper.cs | 482 +++++++++++------- 4 files changed, 301 insertions(+), 192 deletions(-) diff --git a/libs/server/AOF/AofProcessor.cs b/libs/server/AOF/AofProcessor.cs index 6989aeca7a..f2d573693f 100644 --- a/libs/server/AOF/AofProcessor.cs +++ b/libs/server/AOF/AofProcessor.cs @@ -113,7 +113,7 @@ private unsafe void RecoverReplay(int dbId, long untilAddress) try { int count = 0; - storeWrapper.TryGetOrSetDatabase(dbId, out var db); + storeWrapper.TryGetOrAddDatabase(dbId, out var db); var appendOnlyFile = db.AppendOnlyFile; SwitchActiveDatabaseContext(dbId); diff --git a/libs/server/Metrics/Info/GarnetInfoMetrics.cs b/libs/server/Metrics/Info/GarnetInfoMetrics.cs index 1203145682..255727ab14 100644 --- a/libs/server/Metrics/Info/GarnetInfoMetrics.cs +++ b/libs/server/Metrics/Info/GarnetInfoMetrics.cs @@ -55,7 +55,7 @@ private void PopulateServerInfo(StoreWrapper storeWrapper) new("monitor_task", storeWrapper.serverOptions.MetricsSamplingFrequency > 0 ? "enabled" : "disabled"), new("monitor_freq", storeWrapper.serverOptions.MetricsSamplingFrequency.ToString()), new("latency_monitor", storeWrapper.serverOptions.LatencyMonitor ? "enabled" : "disabled"), - new("run_id", storeWrapper.run_id), + new("run_id", storeWrapper.runId), new("redis_version", storeWrapper.redisProtocolVersion), new("redis_mode", storeWrapper.serverOptions.EnableCluster ? "cluster" : "standalone"), ]; @@ -273,7 +273,7 @@ private void PopulatePersistenceInfo(StoreWrapper storeWrapper) new("FlushedUntilAddress", !aofEnabled ? "N/A" : storeWrapper.appendOnlyFile.FlushedUntilAddress.ToString()), new("BeginAddress", !aofEnabled ? "N/A" : storeWrapper.appendOnlyFile.BeginAddress.ToString()), new("TailAddress", !aofEnabled ? "N/A" : storeWrapper.appendOnlyFile.TailAddress.ToString()), - new("SafeAofAddress", !aofEnabled ? "N/A" : storeWrapper.SafeAofAddress.ToString()) + new("SafeAofAddress", !aofEnabled ? "N/A" : storeWrapper.safeAofAddress.ToString()) ]; } diff --git a/libs/server/Storage/SizeTracker/CacheSizeTracker.cs b/libs/server/Storage/SizeTracker/CacheSizeTracker.cs index 0da622e776..be2b7cc9ef 100644 --- a/libs/server/Storage/SizeTracker/CacheSizeTracker.cs +++ b/libs/server/Storage/SizeTracker/CacheSizeTracker.cs @@ -25,6 +25,7 @@ public class CacheSizeTracker public long TargetSize; public long ReadCacheTargetSize; + int isStarted = 0; private const int deltaFraction = 10; // 10% of target size private TsavoriteKV store; @@ -89,6 +90,10 @@ public CacheSizeTracker(TsavoriteKV - /// Store + /// Store (of DB 0) /// public TsavoriteKV store; /// - /// Object store + /// Object store (of DB 0) /// public TsavoriteKV objectStore; /// - /// Server options + /// AOF (of DB 0) /// - public readonly GarnetServerOptions serverOptions; - internal readonly IClusterProvider clusterProvider; + public TsavoriteLog appendOnlyFile; /// - /// Get server + /// Last save time (of DB 0) /// - public GarnetServerTcp GetTcpServer() => (GarnetServerTcp)server; + public DateTimeOffset lastSaveTime; /// - /// Access control list governing all commands + /// Version map (of DB 0) /// - public readonly AccessControlList accessControlList; + internal WatchVersionMap versionMap; /// - /// AOF + /// Object store size tracker (of DB 0) /// - public TsavoriteLog appendOnlyFile; + internal CacheSizeTracker objectStoreSizeTracker; /// - /// Last save time + /// Server options /// - public DateTimeOffset lastSaveTime; + public readonly GarnetServerOptions serverOptions; /// - /// Logger factory + /// Get server /// - public readonly ILoggerFactory loggerFactory; + public GarnetServerTcp GetTcpServer() => (GarnetServerTcp)server; - internal readonly CollectionItemBroker itemBroker; - internal readonly CustomCommandManager customCommandManager; - internal readonly GarnetServerMonitor monitor; - internal WatchVersionMap versionMap; + /// + /// Access control list governing all commands + /// + public readonly AccessControlList accessControlList; - internal CacheSizeTracker objectStoreSizeTracker; + /// + /// Logger factory + /// + public readonly ILoggerFactory loggerFactory; + /// + /// Object serializer + /// public readonly GarnetObjectSerializer GarnetObjectSerializer; /// @@ -90,46 +95,79 @@ public sealed class StoreWrapper /// public readonly ILogger logger; - internal readonly ILogger sessionLogger; - readonly CancellationTokenSource ctsCommit; - - internal long SafeAofAddress = -1; - - // Standalone instance node_id - internal readonly string run_id; - private SingleWriterMultiReaderLock _checkpointTaskLock; - /// /// Lua script cache /// public readonly ConcurrentDictionary storeScriptCache; + /// + /// Logging frequency + /// public readonly TimeSpan loggingFrequency; /// /// Number of current logical databases /// - public int databaseCount; + public int DatabaseCount => activeDbIdsLength; + + /// + /// Definition for delegate creating a new logical database + /// + public delegate GarnetDatabase DatabaseCreatorDelegate(int dbId, out string storeCheckpointDir, out string aofDir); /// /// Delegate for creating a new logical database /// internal readonly DatabaseCreatorDelegate createDatabasesDelegate; + internal readonly CollectionItemBroker itemBroker; + internal readonly CustomCommandManager customCommandManager; + internal readonly GarnetServerMonitor monitor; + internal readonly IClusterProvider clusterProvider; + internal readonly ILogger sessionLogger; + internal long safeAofAddress = -1; + + // Standalone instance node_id + internal readonly string runId; + + readonly CancellationTokenSource ctsCommit; + SingleWriterMultiReaderLock checkpointTaskLock; + + // True if this server supports more than one logical database readonly bool allowMultiDb; - ExpandableMap databases; - int[] activeDbIds = null; + + // Map of databases by database ID (by default: of size 1, contains only DB 0) + ExpandableMap databases; + + // Array containing active database IDs + int[] activeDbIds; + + // Total number of current active database IDs int activeDbIdsLength; - Task[] checkpointTasks = null; - Task[] aofTasks = null; + + // Last DB ID activated + int lastActivatedDbId = -1; + + // Reusable task array for tracking checkpointing of multiple DBs + // Used by recurring checkpointing task if multiple DBs exist + Task[] checkpointTasks; + + // Reusable task array for tracking aof commits of multiple DBs + // Used by recurring aof commits task if multiple DBs exist + Task[] aofTasks; readonly object activeDbIdsLock = new(); + // File name of serialized binary file containing all DB IDs const string DatabaseIdsFileName = "dbIds.dat"; + + // Path of serialization for the DB IDs file used when committing / recovering to / from AOF readonly string aofDatabaseIdsPath; + + // Path of serialization for the DB IDs file used when committing / recovering to / from a checkpoint readonly string checkpointDatabaseIdsPath; - readonly IStreamProvider databaseIdsStreamProvider; - public delegate GarnetDatabase DatabaseCreatorDelegate(int dbId, out string storeCheckpointDir, out string aofDir); + // Stream provider for serializing DB IDs file + readonly IStreamProvider databaseIdsStreamProvider; /// /// Constructor @@ -165,9 +203,10 @@ public StoreWrapper( this.GarnetObjectSerializer = new GarnetObjectSerializer(this.customCommandManager); this.loggingFrequency = TimeSpan.FromSeconds(serverOptions.LoggingFrequency); + // If more than one database allowed, multi-db mode is turned on this.allowMultiDb = this.serverOptions.MaxDatabases > 1; - // Create databases map + // Create default databases map of size 1 databases = new ExpandableMap(1, 0, this.serverOptions.MaxDatabases - 1); // Create default database of index 0 (unless specified otherwise) @@ -177,7 +216,8 @@ public StoreWrapper( checkpointDatabaseIdsPath = storeCheckpointDir == null ? null : Path.Combine(storeCheckpointDir, DatabaseIdsFileName); aofDatabaseIdsPath = aofPath == null ? null : Path.Combine(aofPath, DatabaseIdsFileName); - if (!this.TrySetDatabase(0, ref db)) + // Set new database in map + if (!this.TryAddDatabase(0, ref db)) throw new GarnetException("Failed to set initial database in databases map"); this.InitializeFieldsFromDatabase(databases.Map[0]); @@ -242,14 +282,14 @@ public StoreWrapper( if (clusterFactory != null) clusterProvider = clusterFactory.CreateClusterProvider(this); ctsCommit = new(); - run_id = Generator.CreateHexId(); + runId = Generator.CreateHexId(); } /// /// Copy Constructor /// /// Source instance - /// + /// Enable AOF in StoreWrapper copy public StoreWrapper(StoreWrapper storeWrapper, bool recordToAof) : this( storeWrapper.version, storeWrapper.redisProtocolVersion, @@ -266,32 +306,6 @@ public StoreWrapper(StoreWrapper storeWrapper, bool recordToAof) : this( this.CopyDatabases(storeWrapper, recordToAof); } - private void CopyDatabases(StoreWrapper src, bool recordToAof) - { - var databasesMapSize = src.databases.ActualSize; - var databasesMapSnapshot = src.databases.Map; - - for (var dbId = 0; dbId < databasesMapSize; dbId++) - { - var db = databasesMapSnapshot[dbId]; - var dbCopy = new GarnetDatabase(db.MainStore, db.ObjectStore, db.ObjectStoreSizeTracker, - recordToAof ? db.AofDevice : null, recordToAof ? db.AppendOnlyFile : null); - this.TrySetDatabase(dbId, ref dbCopy); - } - - InitializeFieldsFromDatabase(databases.Map[0]); - } - - private void InitializeFieldsFromDatabase(GarnetDatabase db) - { - // Set fields to default database - this.store = db.MainStore; - this.objectStore = db.ObjectStore; - this.objectStoreSizeTracker = db.ObjectStoreSizeTracker; - this.appendOnlyFile = db.AppendOnlyFile; - this.versionMap = db.VersionMap; - } - /// /// Get IP /// @@ -327,7 +341,7 @@ internal FunctionsState CreateFunctionsState(int dbId = 0) if (dbId == 0) return new(appendOnlyFile, versionMap, customCommandManager, null, objectStoreSizeTracker, GarnetObjectSerializer); - if (!this.TryGetOrSetDatabase(dbId, out var db)) + if (!this.TryGetOrAddDatabase(dbId, out var db)) throw new GarnetException($"Database with ID {dbId} was not found."); return new(db.AppendOnlyFile, db.VersionMap, customCommandManager, null, db.ObjectStoreSizeTracker, @@ -364,6 +378,7 @@ public void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStore { if (replicaRecover) { + // Note: Since replicaRecover only pertains to cluster-mode, we can use the default store pointers (since multi-db mode is disabled in cluster-mode) if (metadata.storeIndexToken != default && metadata.storeHlogToken != default) { storeVersion = !recoverMainStoreFromToken ? store.Recover() : store.Recover(metadata.storeIndexToken, metadata.storeHlogToken); @@ -417,46 +432,6 @@ public void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStore } } - private void WriteDatabaseIdsSnapshot(string path) - { - var activeDbIdsSize = activeDbIdsLength; - var activeDbIdsSnapshot = activeDbIds; - - var dbIdsWithLength = new int[activeDbIdsSize]; - dbIdsWithLength[0] = activeDbIdsSize - 1; - Array.Copy(activeDbIdsSnapshot, 1, dbIdsWithLength, 1, activeDbIdsSize - 1); - - var dbIdData = new byte[sizeof(int) * dbIdsWithLength.Length]; - - Buffer.BlockCopy(dbIdsWithLength, 0, dbIdData, 0, dbIdData.Length); - databaseIdsStreamProvider.Write(path, dbIdData); - } - - private void RecoverDatabases(string path) - { - using var stream = databaseIdsStreamProvider.Read(path); - using var streamReader = new BinaryReader(stream); - - if (streamReader.BaseStream.Length > 0) - { - var idsCount = streamReader.ReadInt32(); - var dbIds = new int[idsCount]; - var dbIdData = streamReader.ReadBytes((int)streamReader.BaseStream.Length); - Buffer.BlockCopy(dbIdData, 0, dbIds, 0, dbIdData.Length); - - var databasesMapSize = databases.ActualSize; - var databasesMapSnapshot = databases.Map; - - foreach (var dbId in dbIds) - { - if (dbId < databasesMapSize && !databasesMapSnapshot[dbId].IsDefault()) - continue; - - this.TryGetOrSetDatabase(dbId, out _); - } - } - } - /// /// Recover AOF /// @@ -465,12 +440,19 @@ public void RecoverAOF() if (allowMultiDb && aofDatabaseIdsPath != null) RecoverDatabases(aofDatabaseIdsPath); - var databasesMapSize = databases.ActualSize; var databasesMapSnapshot = databases.Map; - for (var i = 0; i < databasesMapSize; i++) + + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + + for (var i = 0; i < activeDbIdsSize; i++) { - var db = databasesMapSnapshot[i]; + var dbId = activeDbIdsSnapshot[i]; + var db = databasesMapSnapshot[dbId]; + Debug.Assert(!db.IsDefault()); + if (db.AppendOnlyFile == null) continue; + db.AppendOnlyFile.Recover(); logger?.LogInformation("Recovered AOF: begin address = {beginAddress}, tail address = {tailAddress}", db.AppendOnlyFile.BeginAddress, db.AppendOnlyFile.TailAddress); } @@ -549,6 +531,40 @@ public long ReplayAOF(long untilAddress = -1) return replicationOffset; } + /// + /// Try to add a new database + /// + /// Database ID + /// Database + /// + public bool TryAddDatabase(int dbId, ref GarnetDatabase db) + { + if (!allowMultiDb || !databases.TrySetValue(dbId, ref db)) + return false; + + HandleDatabaseAdded(dbId); + return true; + } + + /// + /// Try to get or add a new database + /// + /// Database ID + /// Database + /// True if database was retrieved or added successfully + public bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db) + { + db = default; + + if (!allowMultiDb || !databases.TryGetOrSet(dbId, () => createDatabasesDelegate(dbId, out _, out _), out db, out var added)) + return false; + + if (added) + HandleDatabaseAdded(dbId); + + return true; + } + async Task AutoCheckpointBasedOnAofSizeLimit(long aofSizeLimit, CancellationToken token = default, ILogger logger = null) { try @@ -561,9 +577,9 @@ async Task AutoCheckpointBasedOnAofSizeLimit(long aofSizeLimit, CancellationToke if (token.IsCancellationRequested) break; var aofSizeAtLimit = -1l; - var databasesMapSize = databases.ActualSize; + var activeDbIdsSize = activeDbIdsLength; - if (!allowMultiDb || databasesMapSize == 1) + if (!allowMultiDb || activeDbIdsSize == 1) { var dbAofSize = appendOnlyFile.TailAddress - appendOnlyFile.BeginAddress; if (dbAofSize > aofSizeLimit) @@ -579,10 +595,9 @@ async Task AutoCheckpointBasedOnAofSizeLimit(long aofSizeLimit, CancellationToke } var databasesMapSnapshot = databases.Map; - var activeDbIdsSize = activeDbIdsLength; var activeDbIdsSnapshot = activeDbIds; - if (dbIdsToCheckpoint == null || dbIdsToCheckpoint.Length < databasesMapSize) + if (dbIdsToCheckpoint == null || dbIdsToCheckpoint.Length < activeDbIdsSize) dbIdsToCheckpoint = new int[activeDbIdsSize]; var dbIdsIdx = 0; @@ -628,9 +643,9 @@ async Task CommitTask(int commitFrequencyMs, ILogger logger = null, Cancellation } else { - var databasesMapSize = databases.ActualSize; + var activeDbIdsSize = this.activeDbIdsLength; - if (!allowMultiDb || databasesMapSize == 1) + if (!allowMultiDb || activeDbIdsSize == 1) { await appendOnlyFile.CommitAsync(null, token); } @@ -649,6 +664,12 @@ async Task CommitTask(int commitFrequencyMs, ILogger logger = null, Cancellation } } + /// + /// Perform AOF commits on all active databases + /// + /// Optional reference to pre-allocated array of tasks to use + /// Cancellation token + /// True if should wait until all tasks complete void MultiDatabaseCommit(ref Task[] tasks, CancellationToken token, bool spinWait = true) { var databasesMapSnapshot = databases.Map; @@ -899,36 +920,40 @@ void CompactionCommitAof(ref GarnetDatabase db) } } + /// + /// Commit AOF for all active databases + /// + /// True if should wait until all commits complete internal void CommitAOF(bool spinWait) { if (!serverOptions.EnableAOF || appendOnlyFile == null) return; - var databasesMapSize = databases.ActualSize; - if (!allowMultiDb || databasesMapSize == 1) + var activeDbIdsSize = this.activeDbIdsLength; + if (!allowMultiDb || activeDbIdsSize == 1) { appendOnlyFile.Commit(spinWait); return; } Task[] tasks = null; - MultiDatabaseCommit(ref tasks, CancellationToken.None, spinWait); } + /// + /// Wait for commits from all active databases + /// internal void WaitForCommit() { if (!serverOptions.EnableAOF || appendOnlyFile == null) return; - var databasesMapSize = databases.ActualSize; - if (!allowMultiDb || databasesMapSize == 1) + var activeDbIdsSize = this.activeDbIdsLength; + if (!allowMultiDb || activeDbIdsSize == 1) { appendOnlyFile.WaitForCommit(); return; } var databasesMapSnapshot = databases.Map; - - var activeDbIdsSize = activeDbIdsLength; var activeDbIdsSnapshot = activeDbIds; var tasks = new Task[activeDbIdsSize]; @@ -942,19 +967,23 @@ internal void WaitForCommit() Task.WaitAll(tasks); } + /// + /// Asynchronously wait for commits from all active databases + /// + /// Cancellation token + /// ValueTask internal async ValueTask WaitForCommitAsync(CancellationToken token = default) { if (!serverOptions.EnableAOF || appendOnlyFile == null) return; - var databasesMapSize = databases.ActualSize; - if (!allowMultiDb || databasesMapSize == 1) + var activeDbIdsSize = this.activeDbIdsLength; + if (!allowMultiDb || activeDbIdsSize == 1) { await appendOnlyFile.WaitForCommitAsync(token: token); + return; } var databasesMapSnapshot = databases.Map; - - var activeDbIdsSize = activeDbIdsLength; var activeDbIdsSnapshot = activeDbIds; var tasks = new Task[activeDbIdsSize]; @@ -968,19 +997,23 @@ internal async ValueTask WaitForCommitAsync(CancellationToken token = default) await Task.WhenAll(tasks); } + /// + /// Asynchronously wait for AOF commits on all active databases + /// + /// Cancellation token + /// ValueTask internal async ValueTask CommitAOFAsync(CancellationToken token = default) { if (!serverOptions.EnableAOF || appendOnlyFile == null) return; - var databasesMapSize = databases.ActualSize; - if (!allowMultiDb || databasesMapSize == 1) + var activeDbIdsSize = this.activeDbIdsLength; + if (!allowMultiDb || activeDbIdsSize == 1) { await appendOnlyFile.CommitAsync(token: token); + return; } var databasesMapSnapshot = databases.Map; - - var activeDbIdsSize = activeDbIdsLength; var activeDbIdsSnapshot = activeDbIds; var tasks = new Task[activeDbIdsSize]; @@ -1025,17 +1058,16 @@ internal void Start() Task.Run(() => IndexAutoGrowTask(ctsCommit.Token)); } - var databasesMapSize = databases.ActualSize; + objectStoreSizeTracker?.Start(ctsCommit.Token); - if (!allowMultiDb || databasesMapSize == 1) - objectStoreSizeTracker?.Start(ctsCommit.Token); - else + var activeDbIdsSize = activeDbIdsLength; + + if (allowMultiDb && activeDbIdsSize > 1) { var databasesMapSnapshot = databases.Map; - var activeDbIdsSize = activeDbIdsLength; var activeDbIdsSnapshot = activeDbIds; - for (var i = 0; i < activeDbIdsSize; i++) + for (var i = 1; i < activeDbIdsSize; i++) { var dbId = activeDbIdsSnapshot[i]; var db = databasesMapSnapshot[dbId]; @@ -1145,7 +1177,7 @@ private bool GrowIndexIfNeeded(StoreType storeType, long indexMaxSize, long over public void Dispose() { // Wait for checkpoints to complete and disable checkpointing - _checkpointTaskLock.WriteLock(); + checkpointTaskLock.WriteLock(); // Disable changes to databases map and dispose all databases databases.mapLock.WriteLock(); @@ -1168,13 +1200,13 @@ public void Dispose() /// /// public bool TryPauseCheckpoints() - => _checkpointTaskLock.TryWriteLock(); + => checkpointTaskLock.TryWriteLock(); /// /// Release checkpoint task lock /// public void ResumeCheckpoints() - => _checkpointTaskLock.WriteUnlock(); + => checkpointTaskLock.WriteUnlock(); /// /// Take a checkpoint if no checkpoint was taken after the provided time offset @@ -1202,7 +1234,6 @@ public async Task TakeOnDemandCheckpoint(DateTimeOffset entryTime, int dbId = 0) /// /// Take checkpoint /// - /// /// /// /// @@ -1229,9 +1260,9 @@ public bool TakeCheckpoint(ref int[] dbIds, ref Task[] tasks, StoreType storeTyp /// public bool TakeCheckpoint(bool background, StoreType storeType = StoreType.All, int dbId = -1, ILogger logger = null, CancellationToken token = default) { - var databasesMapSize = databases.ActualSize; + var activeDbIdsSize = activeDbIdsLength; - if (!allowMultiDb || databasesMapSize == 1 || dbId != -1) + if (!allowMultiDb || activeDbIdsSize == 1 || dbId != -1) { var checkpointTask = Task.Run(async () => await CheckpointTask(StoreType.All, dbId, logger: logger), token); if (background) @@ -1241,7 +1272,6 @@ public bool TakeCheckpoint(bool background, StoreType storeType = StoreType.All, return true; } - var activeDbIdsSize = activeDbIdsLength; var activeDbIdsSnapshot = activeDbIds; var tasks = new Task[activeDbIdsSize]; @@ -1452,6 +1482,12 @@ public bool HasKeysInSlots(List slots, int dbId = 0) return false; } + /// + /// Get database stores by DB ID + /// + /// DB Id + /// Main store + /// Object store public void GetDatabaseStores(int dbId, out TsavoriteKV mainStore, out TsavoriteKV objStore) @@ -1463,76 +1499,144 @@ public void GetDatabaseStores(int dbId, } else { - var dbFound = this.TryGetOrSetDatabase(dbId, out var db); + var dbFound = this.TryGetOrAddDatabase(dbId, out var db); Debug.Assert(dbFound); mainStore = db.MainStore; objStore = db.ObjectStore; } } - private void HandleDatabaseAdded(int dbId) + /// + /// Copy active databases from specified StoreWrapper instance + /// + /// Source StoreWrapper + /// True if should enable AOF in copied databases + private void CopyDatabases(StoreWrapper src, bool recordToAof) { - lock (activeDbIdsLock) + var databasesMapSize = src.databases.ActualSize; + var databasesMapSnapshot = src.databases.Map; + + for (var dbId = 0; dbId < databasesMapSize; dbId++) { - databaseCount++; - var db = databases.Map[dbId]; - if (dbId != 0 && objectStoreSizeTracker != null && !objectStoreSizeTracker.Stopped && - db.ObjectStoreSizeTracker != null && db.ObjectStoreSizeTracker.Stopped) - db.ObjectStoreSizeTracker?.Start(ctsCommit.Token); + var db = databasesMapSnapshot[dbId]; + if (db.IsDefault()) continue; + + var dbCopy = new GarnetDatabase(db.MainStore, db.ObjectStore, db.ObjectStoreSizeTracker, + recordToAof ? db.AofDevice : null, recordToAof ? db.AppendOnlyFile : null); + this.TryAddDatabase(dbId, ref dbCopy); + } + + InitializeFieldsFromDatabase(databases.Map[0]); + } + + /// + /// Initializes default fields to those of a specified database (i.e. DB 0) + /// + /// Input database + private void InitializeFieldsFromDatabase(GarnetDatabase db) + { + // Set fields to default database + this.store = db.MainStore; + this.objectStore = db.ObjectStore; + this.objectStoreSizeTracker = db.ObjectStoreSizeTracker; + this.appendOnlyFile = db.AppendOnlyFile; + this.versionMap = db.VersionMap; + } - if (activeDbIds != null && databaseCount < activeDbIds.Length) + /// + /// Serializes an array of active DB IDs to file (excluding DB 0) + /// + /// Path of serialized file + private void WriteDatabaseIdsSnapshot(string path) + { + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + + var dbIdsWithLength = new int[activeDbIdsSize]; + dbIdsWithLength[0] = activeDbIdsSize - 1; + Array.Copy(activeDbIdsSnapshot, 1, dbIdsWithLength, 1, activeDbIdsSize - 1); + + var dbIdData = new byte[sizeof(int) * dbIdsWithLength.Length]; + + Buffer.BlockCopy(dbIdsWithLength, 0, dbIdData, 0, dbIdData.Length); + databaseIdsStreamProvider.Write(path, dbIdData); + } + + /// + /// Recover databases from serialized DB IDs file + /// + /// Path of serialized file + private void RecoverDatabases(string path) + { + using var stream = databaseIdsStreamProvider.Read(path); + using var streamReader = new BinaryReader(stream); + + if (streamReader.BaseStream.Length > 0) + { + // Read length + var idsCount = streamReader.ReadInt32(); + + // Read DB IDs + var dbIds = new int[idsCount]; + var dbIdData = streamReader.ReadBytes((int)streamReader.BaseStream.Length); + Buffer.BlockCopy(dbIdData, 0, dbIds, 0, dbIdData.Length); + + // Add databases + foreach (var dbId in dbIds) { - activeDbIds[databaseCount - 1] = dbId; - activeDbIdsLength++; - return; + this.TryGetOrAddDatabase(dbId, out _); } + } + } + /// + /// Handle a new database added + /// + /// ID of database added + private void HandleDatabaseAdded(int dbId) + { + // If size tracker exists and is stopped, start it (only if DB 0 size tracker is started as well) + var db = databases.Map[dbId]; + if (dbId != 0 && objectStoreSizeTracker != null && !objectStoreSizeTracker.Stopped && + db.ObjectStoreSizeTracker != null && db.ObjectStoreSizeTracker.Stopped) + db.ObjectStoreSizeTracker.Start(ctsCommit.Token); + + var dbIdIdx = Interlocked.Increment(ref lastActivatedDbId); + + // If there is no size increase needed for activeDbIds, set the added ID in the array + if (activeDbIds != null && dbIdIdx < activeDbIds.Length) + { + activeDbIds[dbIdIdx] = dbId; + Interlocked.Increment(ref activeDbIdsLength); + return; + } + + lock (activeDbIdsLock) + { + // Select the next size of activeDbIds (as multiple of 2 from the existing size) var newSize = activeDbIds?.Length ?? 1; - while (databaseCount >= newSize) + while (dbIdIdx + 1 > newSize) { newSize = Math.Min(this.serverOptions.MaxDatabases, newSize * 2); } + // Set an updated instance of activeDbIds + var activeDbIdsSnapshot = activeDbIds; var activeDbIdsUpdated = new int[newSize]; - var databasesMapSize = databases.ActualSize; - var databasesMapSnapshot = databases.Map; - - var activeDbIdsIdx = 0; - for (var i = 0; i < databasesMapSize; i++) + + if (activeDbIdsSnapshot != null) { - var currDb = databasesMapSnapshot[i]; - if (currDb.IsDefault()) - continue; - activeDbIdsUpdated[activeDbIdsIdx++] = i; + Array.Copy(activeDbIdsSnapshot, activeDbIdsUpdated, dbIdIdx); } + // Set the last added ID + activeDbIdsUpdated[dbIdIdx] = dbId; + checkpointTasks = new Task[newSize]; aofTasks = new Task[newSize]; activeDbIds = activeDbIdsUpdated; - activeDbIdsLength = activeDbIdsIdx; + activeDbIdsLength = dbIdIdx + 1; } } - - public bool TrySetDatabase(int dbId, ref GarnetDatabase db) - { - if (!allowMultiDb || !databases.TrySetValue(dbId, ref db)) - return false; - - HandleDatabaseAdded(dbId); - return true; - } - - public bool TryGetOrSetDatabase(int dbId, out GarnetDatabase db) - { - db = default; - - if (!allowMultiDb || !databases.TryGetOrSet(dbId, () => createDatabasesDelegate(dbId, out _, out _), out db, out var added)) - return false; - - if (added) - HandleDatabaseAdded(dbId); - - return true; - } } } \ No newline at end of file From c31b7bfa87ff7b3dc25bcaa9b9489861bf06e01a Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 6 Feb 2025 14:24:37 -0800 Subject: [PATCH 16/82] bugfixes --- libs/server/StoreWrapper.cs | 180 ++++++++++++--------- test/Garnet.test/RespAdminCommandsTests.cs | 2 +- test/Garnet.test/RespTests.cs | 4 +- test/Garnet.test/RespTlsTests.cs | 4 +- 4 files changed, 105 insertions(+), 85 deletions(-) diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index b911e3d2f1..5b67397828 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq.Expressions; using System.Net; using System.Net.Sockets; using System.Threading; @@ -228,19 +229,6 @@ public StoreWrapper( databaseIdsStreamProvider = serverOptions.StreamProviderCreator(); } - if (logger != null) - { - var configMemoryLimit = (store.IndexSize * 64) + store.Log.MaxMemorySizeBytes + - (store.ReadCache?.MaxMemorySizeBytes ?? 0) + - (appendOnlyFile?.MaxMemorySizeBytes ?? 0); - if (objectStore != null) - configMemoryLimit += (objectStore.IndexSize * 64) + objectStore.Log.MaxMemorySizeBytes + - (objectStore.ReadCache?.MaxMemorySizeBytes ?? 0) + - (objectStoreSizeTracker?.TargetSize ?? 0) + - (objectStoreSizeTracker?.ReadCacheTargetSize ?? 0); - logger.LogInformation("Total configured memory limit: {configMemoryLimit}", configMemoryLimit); - } - if (!serverOptions.DisableObjects) this.itemBroker = new CollectionItemBroker(); @@ -302,7 +290,6 @@ public StoreWrapper(StoreWrapper storeWrapper, bool recordToAof) : this( null, storeWrapper.loggerFactory) { - this.clusterProvider = storeWrapper.clusterProvider; this.CopyDatabases(storeWrapper, recordToAof); } @@ -618,7 +605,7 @@ async Task AutoCheckpointBasedOnAofSizeLimit(long aofSizeLimit, CancellationToke if (dbIdsIdx > 0) { logger?.LogInformation("Enforcing AOF size limit currentAofSize: {currAofSize} > AofSizeLimit: {AofSizeLimit}", aofSizeAtLimit, aofSizeLimit); - TakeCheckpoint( ref dbIdsToCheckpoint, ref checkpointTasks, logger: logger, token: token); + CheckpointDatabases(StoreType.All, ref dbIdsToCheckpoint, ref checkpointTasks, logger: logger, token: token); } } } @@ -1179,11 +1166,6 @@ public void Dispose() // Wait for checkpoints to complete and disable checkpointing checkpointTaskLock.WriteLock(); - // Disable changes to databases map and dispose all databases - databases.mapLock.WriteLock(); - foreach (var db in databases.Map) - db.Dispose(); - itemBroker?.Dispose(); monitor?.Dispose(); ctsCommit?.Cancel(); @@ -1191,6 +1173,11 @@ public void Dispose() while (objectStoreSizeTracker != null && !objectStoreSizeTracker.Stopped) Thread.Yield(); + // Disable changes to databases map and dispose all databases + databases.mapLock.WriteLock(); + foreach (var db in databases.Map) + db.Dispose(); + ctsCommit?.Dispose(); clusterProvider?.Dispose(); } @@ -1228,25 +1215,7 @@ public async Task TakeOnDemandCheckpoint(DateTimeOffset entryTime, int dbId = 0) } // Necessary to take a checkpoint because the latest checkpoint is before entryTime - await CheckpointTask(StoreType.All, dbId, logger: logger); - } - - /// - /// Take checkpoint - /// - /// - /// - /// - /// - /// - /// - public bool TakeCheckpoint(ref int[] dbIds, ref Task[] tasks, StoreType storeType = StoreType.All, ILogger logger = null, CancellationToken token = default) - { - // Prevent parallel checkpoint - if (!TryPauseCheckpoints()) return false; - - MultiDatabaseCheckpoint(storeType, ref dbIds, ref tasks, logger, token); - return true; + await CheckpointTask(StoreType.All, dbId, lockAcquired: true, logger: logger); } /// @@ -1264,7 +1233,8 @@ public bool TakeCheckpoint(bool background, StoreType storeType = StoreType.All, if (!allowMultiDb || activeDbIdsSize == 1 || dbId != -1) { - var checkpointTask = Task.Run(async () => await CheckpointTask(StoreType.All, dbId, logger: logger), token); + if (dbId == -1) dbId = 0; + var checkpointTask = Task.Run(async () => await CheckpointTask(storeType, dbId, logger: logger), token); if (background) return true; @@ -1275,50 +1245,28 @@ public bool TakeCheckpoint(bool background, StoreType storeType = StoreType.All, var activeDbIdsSnapshot = activeDbIds; var tasks = new Task[activeDbIdsSize]; - return TakeCheckpoint(ref activeDbIdsSnapshot, ref tasks, logger: logger, token: token); - } - - private async Task CheckpointDatabaseTask(int dbId, StoreType storeType, ILogger logger) - { - var databasesMapSnapshot = databases.Map; - var db = databasesMapSnapshot[dbId]; - if (db.IsDefault()) return; - - DoCompaction(ref db); - var lastSaveStoreTailAddress = db.MainStore.Log.TailAddress; - var lastSaveObjectStoreTailAddress = (db.ObjectStore?.Log.TailAddress).GetValueOrDefault(); - - var full = db.LastSaveStoreTailAddress == 0 || - lastSaveStoreTailAddress - db.LastSaveStoreTailAddress >= serverOptions.FullCheckpointLogInterval || - (db.ObjectStore != null && (db.LastSaveObjectStoreTailAddress == 0 || - lastSaveObjectStoreTailAddress - db.LastSaveObjectStoreTailAddress >= serverOptions.FullCheckpointLogInterval)); - - var tryIncremental = serverOptions.EnableIncrementalSnapshots; - if (db.MainStore.IncrementalSnapshotTailAddress >= serverOptions.IncrementalSnapshotLogSizeLimit) - tryIncremental = false; - if (db.ObjectStore?.IncrementalSnapshotTailAddress >= serverOptions.IncrementalSnapshotLogSizeLimit) - tryIncremental = false; - - var checkpointType = serverOptions.UseFoldOverCheckpoints ? CheckpointType.FoldOver : CheckpointType.Snapshot; - await InitiateCheckpoint(dbId, db, full, checkpointType, tryIncremental, storeType, logger); - if (full) - { - if (storeType is StoreType.Main or StoreType.All) - db.LastSaveStoreTailAddress = lastSaveStoreTailAddress; - if (storeType is StoreType.Object or StoreType.All) - db.LastSaveObjectStoreTailAddress = lastSaveObjectStoreTailAddress; - } - - var lastSave = DateTimeOffset.UtcNow; - if (dbId == 0) - lastSaveTime = lastSave; - db.LastSaveTime = lastSave; + return CheckpointDatabases(storeType, ref activeDbIdsSnapshot, ref tasks, logger: logger, token: token); } - private async Task CheckpointTask(StoreType storeType, int dbId = 0, ILogger logger = null) + /// + /// Asynchronously checkpoint a single database + /// + /// Store type to checkpoint + /// ID of database to checkpoint (default: DB 0) + /// True if lock previously acquired + /// Logger + /// Task + private async Task CheckpointTask(StoreType storeType, int dbId = 0, bool lockAcquired = false, ILogger logger = null) { try { + if (!lockAcquired) + { + // Take lock to ensure no other task will be taking a checkpoint + while (!TryPauseCheckpoints()) + await Task.Yield(); + } + await CheckpointDatabaseTask(dbId, storeType, logger); if (checkpointDatabaseIdsPath != null) @@ -1334,12 +1282,24 @@ private async Task CheckpointTask(StoreType storeType, int dbId = 0, ILogger log } } - private void MultiDatabaseCheckpoint(StoreType storeType, ref int[] dbIds, ref Task[] tasks, ILogger logger = null, CancellationToken token = default) + /// + /// Asynchronously checkpoint multiple databases and wait for all to complete + /// + /// Store type to checkpoint + /// IDs of active databases to checkpoint + /// Optional tasks to use for asynchronous execution (must be the same size as dbIds) + /// Logger + /// Cancellation token + /// False if checkpointing already in progress + private bool CheckpointDatabases(StoreType storeType, ref int[] dbIds, ref Task[] tasks, ILogger logger = null, CancellationToken token = default) { try { Debug.Assert(tasks != null); + // Prevent parallel checkpoint + if (!TryPauseCheckpoints()) return false; + var currIdx = 0; if (dbIds == null) { @@ -1375,6 +1335,52 @@ private void MultiDatabaseCheckpoint(StoreType storeType, ref int[] dbIds, ref T { ResumeCheckpoints(); } + + return true; + } + + /// + /// Asynchronously checkpoint a single database + /// + /// ID of database to checkpoint + /// Store type to checkpoint + /// Logger + /// Task + private async Task CheckpointDatabaseTask(int dbId, StoreType storeType, ILogger logger) + { + var databasesMapSnapshot = databases.Map; + var db = databasesMapSnapshot[dbId]; + if (db.IsDefault()) return; + + DoCompaction(ref db); + var lastSaveStoreTailAddress = db.MainStore.Log.TailAddress; + var lastSaveObjectStoreTailAddress = (db.ObjectStore?.Log.TailAddress).GetValueOrDefault(); + + var full = db.LastSaveStoreTailAddress == 0 || + lastSaveStoreTailAddress - db.LastSaveStoreTailAddress >= serverOptions.FullCheckpointLogInterval || + (db.ObjectStore != null && (db.LastSaveObjectStoreTailAddress == 0 || + lastSaveObjectStoreTailAddress - db.LastSaveObjectStoreTailAddress >= serverOptions.FullCheckpointLogInterval)); + + var tryIncremental = serverOptions.EnableIncrementalSnapshots; + if (db.MainStore.IncrementalSnapshotTailAddress >= serverOptions.IncrementalSnapshotLogSizeLimit) + tryIncremental = false; + if (db.ObjectStore?.IncrementalSnapshotTailAddress >= serverOptions.IncrementalSnapshotLogSizeLimit) + tryIncremental = false; + + var checkpointType = serverOptions.UseFoldOverCheckpoints ? CheckpointType.FoldOver : CheckpointType.Snapshot; + await InitiateCheckpoint(dbId, db, full, checkpointType, tryIncremental, storeType, logger); + if (full) + { + if (storeType is StoreType.Main or StoreType.All) + db.LastSaveStoreTailAddress = lastSaveStoreTailAddress; + if (storeType is StoreType.Object or StoreType.All) + db.LastSaveObjectStoreTailAddress = lastSaveObjectStoreTailAddress; + } + + var lastSave = DateTimeOffset.UtcNow; + if (dbId == 0) + lastSaveTime = lastSave; + db.LastSaveTime = lastSave; } private async Task InitiateCheckpoint(int dbId, GarnetDatabase db, bool full, CheckpointType checkpointType, bool tryIncremental, StoreType storeType, ILogger logger = null) @@ -1541,6 +1547,20 @@ private void InitializeFieldsFromDatabase(GarnetDatabase db) this.objectStoreSizeTracker = db.ObjectStoreSizeTracker; this.appendOnlyFile = db.AppendOnlyFile; this.versionMap = db.VersionMap; + + // Log configured memory limit for each database + if (logger != null) + { + var configMemoryLimit = (store.IndexSize * 64) + store.Log.MaxMemorySizeBytes + + (store.ReadCache?.MaxMemorySizeBytes ?? 0) + + (appendOnlyFile?.MaxMemorySizeBytes ?? 0); + if (objectStore != null) + configMemoryLimit += (objectStore.IndexSize * 64) + objectStore.Log.MaxMemorySizeBytes + + (objectStore.ReadCache?.MaxMemorySizeBytes ?? 0) + + (objectStoreSizeTracker?.TargetSize ?? 0) + + (objectStoreSizeTracker?.ReadCacheTargetSize ?? 0); + logger.LogInformation("Total configured memory limit: {configMemoryLimit}", configMemoryLimit); + } } /// diff --git a/test/Garnet.test/RespAdminCommandsTests.cs b/test/Garnet.test/RespAdminCommandsTests.cs index 7bf4f07f06..6431fc6f8e 100644 --- a/test/Garnet.test/RespAdminCommandsTests.cs +++ b/test/Garnet.test/RespAdminCommandsTests.cs @@ -598,7 +598,7 @@ public async Task SeFlushDbAndFlushAllTest2([Values(RespCommand.FLUSHALL, RespCo [TestCase("save", "")] [TestCase("appendonly", "no")] [TestCase("slave-read-only", "no")] - [TestCase("databases", "1")] + [TestCase("databases", "16")] [TestCase("cluster-node-timeout", "60")] public void SimpleConfigGet(string parameter, string parameterValue) { diff --git a/test/Garnet.test/RespTests.cs b/test/Garnet.test/RespTests.cs index 8e45ccba29..8cc6557fd3 100644 --- a/test/Garnet.test/RespTests.cs +++ b/test/Garnet.test/RespTests.cs @@ -2228,7 +2228,7 @@ public void CanSelectCommand() var db = redis.GetDatabase(0); var reply = db.Execute("SELECT", "0"); ClassicAssert.IsTrue(reply.ToString() == "OK"); - Assert.Throws(() => db.Execute("SELECT", "1")); + Assert.Throws(() => db.Execute("SELECT", "17")); //select again the def db db.Execute("SELECT", "0"); @@ -2240,7 +2240,7 @@ public void CanSelectCommandLC() using var lightClientRequest = TestUtils.CreateRequest(countResponseType: CountResponseType.Bytes); var expectedResponse = "-ERR invalid database index.\r\n+PONG\r\n"; - var response = lightClientRequest.Execute("SELECT 1", "PING", expectedResponse.Length); + var response = lightClientRequest.Execute("SELECT 17", "PING", expectedResponse.Length); ClassicAssert.AreEqual(expectedResponse, response); } diff --git a/test/Garnet.test/RespTlsTests.cs b/test/Garnet.test/RespTlsTests.cs index 7e61240516..c456e5b10c 100644 --- a/test/Garnet.test/RespTlsTests.cs +++ b/test/Garnet.test/RespTlsTests.cs @@ -352,7 +352,7 @@ public void TlsCanSelectCommand() var db = redis.GetDatabase(0); var reply = db.Execute("SELECT", "0"); ClassicAssert.IsTrue(reply.ToString() == "OK"); - Assert.Throws(() => db.Execute("SELECT", "1")); + Assert.Throws(() => db.Execute("SELECT", "17")); //select again the def db db.Execute("SELECT", "0"); @@ -364,7 +364,7 @@ public void TlsCanSelectCommandLC() using var lightClientRequest = TestUtils.CreateRequest(useTLS: true, countResponseType: CountResponseType.Bytes); var expectedResponse = "-ERR invalid database index.\r\n+PONG\r\n"; - var response = lightClientRequest.Execute("SELECT 1", "PING", expectedResponse.Length); + var response = lightClientRequest.Execute("SELECT 17", "PING", expectedResponse.Length); ClassicAssert.AreEqual(expectedResponse, response); } From a4a785058e685c28748cb150db305a6a4b2a4ef1 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 6 Feb 2025 14:41:07 -0800 Subject: [PATCH 17/82] wip --- libs/server/Resp/BasicCommands.cs | 7 ++-- libs/server/Servers/StoreApi.cs | 8 ++-- libs/server/Storage/Session/StorageSession.cs | 10 +++-- libs/server/StoreWrapper.cs | 41 +++++++++++-------- 4 files changed, 39 insertions(+), 27 deletions(-) diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index ea4ca8d081..590ca13fb6 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -1668,9 +1668,10 @@ void FlushDb(RespCommand cmd) void ExecuteFlushDb(bool unsafeTruncateLog) { - storeWrapper.GetDatabaseStores(activeDbId, out var mainStore, out var objStore); - mainStore.Log.ShiftBeginAddress(mainStore.Log.TailAddress, truncateLog: unsafeTruncateLog); - objStore?.Log.ShiftBeginAddress(objStore.Log.TailAddress, truncateLog: unsafeTruncateLog); + var dbFound = storeWrapper.TryGetDatabase(activeDbId, out var db); + Debug.Assert(dbFound); + db.MainStore.Log.ShiftBeginAddress(db.MainStore.Log.TailAddress, truncateLog: unsafeTruncateLog); + db.ObjectStore?.Log.ShiftBeginAddress(db.ObjectStore.Log.TailAddress, truncateLog: unsafeTruncateLog); } /// diff --git a/libs/server/Servers/StoreApi.cs b/libs/server/Servers/StoreApi.cs index e6f67b0c1a..c4d9f54665 100644 --- a/libs/server/Servers/StoreApi.cs +++ b/libs/server/Servers/StoreApi.cs @@ -50,9 +50,11 @@ public StoreApi(StoreWrapper storeWrapper) /// public void FlushDB(int dbId = 0, bool unsafeTruncateLog = false) { - storeWrapper.GetDatabaseStores(dbId, out var mainStore, out var objStore); - mainStore.Log.ShiftBeginAddress(mainStore.Log.TailAddress, truncateLog: unsafeTruncateLog); - objStore?.Log.ShiftBeginAddress(objStore.Log.TailAddress, truncateLog: unsafeTruncateLog); + var dbFound = storeWrapper.TryGetDatabase(dbId, out var db); + if (!dbFound) return; + + db.MainStore.Log.ShiftBeginAddress(db.MainStore.Log.TailAddress, truncateLog: unsafeTruncateLog); + db.ObjectStore?.Log.ShiftBeginAddress(db.ObjectStore.Log.TailAddress, truncateLog: unsafeTruncateLog); } } } \ No newline at end of file diff --git a/libs/server/Storage/Session/StorageSession.cs b/libs/server/Storage/Session/StorageSession.cs index e157b6cd76..86c2437c14 100644 --- a/libs/server/Storage/Session/StorageSession.cs +++ b/libs/server/Storage/Session/StorageSession.cs @@ -73,11 +73,13 @@ public StorageSession(StoreWrapper storeWrapper, var functions = new MainSessionFunctions(functionsState); - storeWrapper.GetDatabaseStores(dbId, out var mainStore, out var objStore); - var session = mainStore.NewSession(functions); + var dbFound = storeWrapper.TryGetDatabase(dbId, out var db); + Debug.Assert(dbFound); + + var session = db.MainStore.NewSession(functions); var objectStoreFunctions = new ObjectSessionFunctions(functionsState); - var objectStoreSession = objStore?.NewSession(objectStoreFunctions); + var objectStoreSession = db.ObjectStore?.NewSession(objectStoreFunctions); basicContext = session.BasicContext; lockableContext = session.LockableContext; @@ -87,7 +89,7 @@ public StorageSession(StoreWrapper storeWrapper, objectStoreLockableContext = objectStoreSession.LockableContext; } - HeadAddress = mainStore.Log.HeadAddress; + HeadAddress = db.MainStore.Log.HeadAddress; ObjectScanCountLimit = storeWrapper.serverOptions.ObjectScanCountLimit; } diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 5b67397828..04149b17ed 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -1450,10 +1450,12 @@ public bool HasKeysInSlots(List slots, int dbId = 0) { if (slots.Count > 0) { - GetDatabaseStores(dbId, out var dbMainStore, out var dbObjectStore); + var dbFound = TryGetDatabase(dbId, out var db); + Debug.Assert(dbFound); + bool hasKeyInSlots = false; { - using var iter = dbMainStore.Iterate>(new SimpleSessionFunctions()); + using var iter = db.MainStore.Iterate>(new SimpleSessionFunctions()); while (!hasKeyInSlots && iter.GetNext(out RecordInfo record)) { ref var key = ref iter.GetKey(); @@ -1465,11 +1467,11 @@ public bool HasKeysInSlots(List slots, int dbId = 0) } } - if (!hasKeyInSlots && dbObjectStore != null) + if (!hasKeyInSlots && db.ObjectStore != null) { var functionsState = CreateFunctionsState(); var objstorefunctions = new ObjectSessionFunctions(functionsState); - var objectStoreSession = dbObjectStore?.NewSession(objstorefunctions); + var objectStoreSession = db.ObjectStore?.NewSession(objstorefunctions); var iter = objectStoreSession.Iterate(); while (!hasKeyInSlots && iter.GetNext(out RecordInfo record)) { @@ -1489,27 +1491,32 @@ public bool HasKeysInSlots(List slots, int dbId = 0) } /// - /// Get database stores by DB ID + /// Get database DB ID /// /// DB Id - /// Main store - /// Object store - public void GetDatabaseStores(int dbId, - out TsavoriteKV mainStore, - out TsavoriteKV objStore) + /// Database + /// True if database found + public bool TryGetDatabase(int dbId, out GarnetDatabase db) { + var databasesMapSize = databases.ActualSize; + var databasesMapSnapshot = databases.Map; + if (dbId == 0) { - mainStore = this.store; - objStore = this.objectStore; + db = databasesMapSnapshot[0]; + Debug.Assert(!db.IsDefault()); + return true; } - else + + // Check if database already exists + if (dbId < databasesMapSize) { - var dbFound = this.TryGetOrAddDatabase(dbId, out var db); - Debug.Assert(dbFound); - mainStore = db.MainStore; - objStore = db.ObjectStore; + db = databasesMapSnapshot[dbId]; + if (!db.IsDefault()) return true; } + + // Try to retrieve or add database + return this.TryGetOrAddDatabase(dbId, out db); } /// From eeefc703e6bf2b70cb2c58a7bf9d933854f92a85 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 6 Feb 2025 16:27:28 -0800 Subject: [PATCH 18/82] tests --- test/Garnet.test/MultiDatabaseTests.cs | 179 ++++++++++++++++++++++++- 1 file changed, 173 insertions(+), 6 deletions(-) diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index 5137ca252e..31ca7b9b9d 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -1,10 +1,14 @@ using System.Linq; using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Text; using System.Threading; +using System.Threading.Tasks; using NUnit.Framework; using NUnit.Framework.Legacy; using StackExchange.Redis; +using Garnet.common; namespace Garnet.test { @@ -27,7 +31,9 @@ public void MultiDatabaseBasicSelectTestSE() var db1Key1 = "db1:key1"; var db1Key2 = "db1:key2"; var db2Key1 = "db2:key1"; - var db2Key2 = "db2:key1"; + var db2Key2 = "db2:key2"; + var db12Key1 = "db12:key1"; + var db12Key2 = "db12:key1"; using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db1 = redis.GetDatabase(0); @@ -44,6 +50,34 @@ public void MultiDatabaseBasicSelectTestSE() ClassicAssert.IsFalse(db1.KeyExists(db2Key1)); ClassicAssert.IsFalse(db1.KeyExists(db2Key2)); + + var db12 = redis.GetDatabase(11); + ClassicAssert.IsFalse(db12.KeyExists(db1Key1)); + ClassicAssert.IsFalse(db12.KeyExists(db1Key2)); + + db2.StringSet(db12Key2, "db12:value2"); + db2.SetAdd(db12Key2, [new RedisValue("db12:val2"), new RedisValue("db12:val2")]); + + ClassicAssert.IsFalse(db12.KeyExists(db12Key1)); + ClassicAssert.IsFalse(db12.KeyExists(db12Key2)); + } + + [Test] + public void MultiDatabaseBasicSelectErroneousTestSE() + { + var db1Key1 = "db1:key1"; + var db1Key2 = "db1:key2"; + var db2Key1 = "db2:key1"; + var db2Key2 = "db2:key1"; + + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db1 = redis.GetDatabase(0); + + db1.StringSet(db1Key1, "db1:value1"); + db1.ListLeftPush(db1Key2, [new RedisValue("db1:val1"), new RedisValue("db1:val2")]); + + var db17 = redis.GetDatabase(17); + Assert.Throws(() => db17.StringSet(db1Key1, "db1:value1"), "The database does not exist on the server: 17"); } [Test] @@ -53,16 +87,22 @@ public void MultiDatabaseSameKeyTestSE() using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db1 = redis.GetDatabase(0); - db1.StringSet(key1, "db1:value1"); + db1.StringSet(key1, "db1:val1"); var db2 = redis.GetDatabase(1); - db2.SetAdd(key1, [new RedisValue("db2:val2"), new RedisValue("db2:val2")]); + db2.SetAdd(key1, [new RedisValue("db2:val1"), new RedisValue("db2:val2")]); + + var db12 = redis.GetDatabase(11); + db12.ListLeftPush(key1, [new RedisValue("db12:val1"), new RedisValue("db12:val2")]); var db1val = db1.StringGet(key1); - ClassicAssert.AreEqual("db1:value1", db1val.ToString()); + ClassicAssert.AreEqual("db1:val1", db1val.ToString()); - var db2val = db2.SetPop(key1); - ClassicAssert.AreEqual("db2:val2", db2val.ToString()); + var db2val = db2.SetMembers(key1); + CollectionAssert.AreEquivalent(db2val, new[] {new RedisValue("db2:val1"), new RedisValue("db2:val2")}); + + var db12val = db12.ListLeftPop(key1); + ClassicAssert.AreEqual("db12:val2", db12val.ToString()); } [Test] @@ -141,6 +181,118 @@ public void MultiDatabaseBasicSelectTestLC() ClassicAssert.AreEqual(expectedResponse, actualValue); } + [Test] + public void MultiDatabaseSelectMultithreadedTestSE() + { + // Create a set of tuples (db-id, key, value) + var dbCount = 16; + var keyCount = 16; + var tuples = GenerateDataset(dbCount, keyCount); + + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + + // In parallel, add each (key, value) pair to a database of id db-id + var kvBag = new ConcurrentBag<(int, string, string)>(tuples); + var tasks = new Task[dbCount * keyCount]; + for (var i = 0; i < tasks.Length; i++) + { + tasks[i] = Task.Run(() => + { + kvBag.TryTake(out var tup); + var db = redis.GetDatabase(tup.Item1); + return db.StringSet(tup.Item2, tup.Item3); + }); + } + + // Wait for all tasks to finish + Task.WaitAll(tasks); + + // Check that all tasks successfully entered the data to the respective database + Assert.That(tasks, Has.All.Matches>(t => t.Result)); + + // In parallel, retrieve the actual value for each db-id and key + kvBag = new(tuples); + for (var i = 0; i < tasks.Length; i++) + { + tasks[i] = Task.Run(() => + { + kvBag.TryTake(out var tup); + var db = redis.GetDatabase(tup.Item1); + var actualValue = db.StringGet(tup.Item2); + return (tup.Item1, tup.Item2, actualValue.ToString()); + }); + } + + // Wait for all tasks to finish + Task.WaitAll(tasks); + + // Check that (db-id, key, actual-value) tuples match original (db-id, key, value) tuples + var results = tasks.Select(t => ((Task<(int, string, string)>)t).Result); + Assert.That(results, Is.EquivalentTo(tuples)); + } + + [Test] + public void MultiDatabaseSelectMultithreadedTestLC() + { + // Create a set of tuples (db-id, key, value) + var dbCount = 16; + var keyCount = 16; + var tuples = GenerateDataset(dbCount, keyCount); + + // Create multiple LC request objects to be used + var lcRequests = new BlockingCollection(); + for (var i = 0; i < 16; i++) + { + lcRequests.Add(TestUtils.CreateRequest(countResponseType: CountResponseType.Bytes)); + } + + // In parallel, add each (key, value) pair to a database of id db-id + var kvBag = new ConcurrentBag<(int, string, string)>(tuples); + var tasks = new Task[dbCount * keyCount]; + for (var i = 0; i < tasks.Length; i++) + { + tasks[i] = Task.Run(() => + { + kvBag.TryTake(out var tup); + var expectedResponse = "+OK\r\n+OK\r\n"; + var lcRequest = lcRequests.Take(); + var response = lcRequest.Execute($"SELECT {tup.Item1}", $"SET {tup.Item2} {tup.Item3}", expectedResponse.Length); + lcRequests.Add(lcRequest); + return expectedResponse == response; + }); + } + + // Wait for all tasks to finish + Task.WaitAll(tasks); + + // Check that all tasks successfully entered the data to the respective database + Assert.That(tasks, Has.All.Matches>(t => t.Result)); + + // In parallel, retrieve the actual value for each db-id and key + kvBag = new(tuples); + for (var i = 0; i < tasks.Length; i++) + { + tasks[i] = Task.Run(() => + { + kvBag.TryTake(out var tup); + var expectedResponse = $"+OK\r\n${tup.Item3.Length}\r\n{tup.Item3}\r\n"; + var lcRequest = lcRequests.Take(); + var response = lcRequest.Execute($"SELECT {tup.Item1}", $"GET {tup.Item2}", expectedResponse.Length); + lcRequests.Add(lcRequest); + return expectedResponse == response; + }); + } + + // Wait for all tasks to finish + Task.WaitAll(tasks); + + // Check that all the tasks retrieved the correct value successfully + Assert.That(tasks, Has.All.Matches>(t => t.Result)); + + while (lcRequests.TryTake(out var client)) + client.Dispose(); + } + [Test] public void MultiDatabaseSaveRecoverObjectTest() { @@ -338,5 +490,20 @@ public void TearDown() server.Dispose(); TestUtils.DeleteDirectory(TestUtils.MethodTestDir); } + + private List<(int, string, string)> GenerateDataset(int dbCount, int keyCount) + { + var data = new List<(int, string, string)>(); + + for (var dbId = 0; dbId < dbCount; dbId++) + { + for (var keyId = 0; keyId < keyCount; keyId++) + { + data.Add((dbId, $"key{keyId}", $"db{dbId}:val{keyId}")); + } + } + + return data; + } } } From 1887d0af06f8db8b0c877a08718c462f1b8b64f0 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 6 Feb 2025 19:05:50 -0800 Subject: [PATCH 19/82] format --- libs/cluster/Server/ClusterProvider.cs | 2 +- libs/common/HashSlotUtils.cs | 2 +- libs/host/GarnetServer.cs | 8 ++++---- libs/server/AOF/AofProcessor.cs | 3 +-- libs/server/GarnetDatabase.cs | 2 +- libs/server/Resp/GarnetDatabaseSession.cs | 2 +- libs/server/Resp/RespServerSession.cs | 2 +- libs/server/StoreWrapper.cs | 19 +++++++++---------- test/Garnet.test/MultiDatabaseTests.cs | 16 ++++++++-------- 9 files changed, 27 insertions(+), 29 deletions(-) diff --git a/libs/cluster/Server/ClusterProvider.cs b/libs/cluster/Server/ClusterProvider.cs index 172c081949..ca732fadf6 100644 --- a/libs/cluster/Server/ClusterProvider.cs +++ b/libs/cluster/Server/ClusterProvider.cs @@ -263,7 +263,7 @@ public MetricsItem[] GetReplicationInfo() if (!serverOptions.EnableCluster) { return (replicationManager.ReplicationOffset, default); - }; + } return (replicationManager.ReplicationOffset, replicationManager.GetReplicaInfo()); } diff --git a/libs/common/HashSlotUtils.cs b/libs/common/HashSlotUtils.cs index 99cf67aa26..3fbb6a6c25 100644 --- a/libs/common/HashSlotUtils.cs +++ b/libs/common/HashSlotUtils.cs @@ -102,7 +102,7 @@ public static unsafe ushort HashSlot(byte* keyPtr, int ksize) var end = keyPtr + ksize; // Find first occurence of '{' - while (startPtr < end && *startPtr != '{') { startPtr++; }; + while (startPtr < end && *startPtr != '{') { startPtr++; } // Return early if did not find '{' if (startPtr == end) return (ushort)(Hash(keyPtr, ksize) & 16383); diff --git a/libs/host/GarnetServer.cs b/libs/host/GarnetServer.cs index b8a248411c..ffdb139d3f 100644 --- a/libs/host/GarnetServer.cs +++ b/libs/host/GarnetServer.cs @@ -321,9 +321,9 @@ private TsavoriteKV mainStoreCheckpointDir = Path.Combine(checkpointDir, "Store"); var baseName = Path.Combine(mainStoreCheckpointDir, $"checkpoints{(dbId == 0 ? string.Empty : $"_{dbId}")}"); var defaultNamingScheme = new DefaultCheckpointNamingScheme(baseName); - - kvSettings.CheckpointManager = opts.EnableCluster ? - clusterFactory.CreateCheckpointManager(checkpointFactory, defaultNamingScheme, isMainStore: true, logger) : + + kvSettings.CheckpointManager = opts.EnableCluster ? + clusterFactory.CreateCheckpointManager(checkpointFactory, defaultNamingScheme, isMainStore: true, logger) : new DeviceLogCommitCheckpointManager(checkpointFactory, defaultNamingScheme, removeOutdated: true); return new TsavoriteKV(kvSettings @@ -348,7 +348,7 @@ private TsavoriteKV>; using MainStoreFunctions = StoreFunctions; @@ -33,7 +32,7 @@ public sealed unsafe partial class AofProcessor private readonly ObjectInput objectStoreInput; private readonly CustomProcedureInput customProcInput; private readonly SessionParseState parseState; - + int activeDbId; /// diff --git a/libs/server/GarnetDatabase.cs b/libs/server/GarnetDatabase.cs index 6d5ecb5c8c..78be12b957 100644 --- a/libs/server/GarnetDatabase.cs +++ b/libs/server/GarnetDatabase.cs @@ -103,4 +103,4 @@ public void Dispose() disposed = true; } } -} +} \ No newline at end of file diff --git a/libs/server/Resp/GarnetDatabaseSession.cs b/libs/server/Resp/GarnetDatabaseSession.cs index 3a1bc4a0fc..e24e44b54e 100644 --- a/libs/server/Resp/GarnetDatabaseSession.cs +++ b/libs/server/Resp/GarnetDatabaseSession.cs @@ -52,4 +52,4 @@ public void Dispose() disposed = true; } } -} +} \ No newline at end of file diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index 995df697e6..4e00a74fe1 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -30,7 +30,7 @@ namespace Garnet.server LockableContext>, GenericAllocator>>>>; - + /// /// RESP server session /// diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 536b377f56..964d46b4fc 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq.Expressions; using System.Net; using System.Net.Sockets; using System.Threading; @@ -142,10 +141,10 @@ public sealed class StoreWrapper // True if this server supports more than one logical database readonly bool allowMultiDb; - + // Map of databases by database ID (by default: of size 1, contains only DB 0) - ExpandableMap databases; - + ExpandableMap databases; + // Array containing active database IDs int[] activeDbIds; @@ -169,7 +168,7 @@ public sealed class StoreWrapper // Path of serialization for the DB IDs file used when committing / recovering to / from AOF readonly string aofDatabaseIdsPath; - + // Path of serialization for the DB IDs file used when committing / recovering to / from a checkpoint readonly string checkpointDatabaseIdsPath; @@ -341,7 +340,7 @@ internal FunctionsState CreateFunctionsState(int dbId = 0) if (dbId == 0) return new(appendOnlyFile, versionMap, customCommandManager, null, objectStoreSizeTracker, GarnetObjectSerializer); - + if (!this.TryGetOrAddDatabase(dbId, out var db)) throw new GarnetException($"Database with ID {dbId} was not found."); @@ -615,7 +614,7 @@ async Task AutoCheckpointBasedOnAofSizeLimit(long aofSizeLimit, CancellationToke break; } } - + if (dbIdsIdx > 0) { logger?.LogInformation("Enforcing AOF size limit currentAofSize: {currAofSize} > AofSizeLimit: {AofSizeLimit}", aofSizeAtLimit, aofSizeLimit); @@ -1251,7 +1250,7 @@ public bool TakeCheckpoint(bool background, StoreType storeType = StoreType.All, var checkpointTask = Task.Run(async () => await CheckpointTask(storeType, dbId, logger: logger), token); if (background) return true; - + checkpointTask.Wait(token); return true; } @@ -1319,7 +1318,7 @@ private bool CheckpointDatabases(StoreType storeType, ref int[] dbIds, ref Task[ { var activeDbIdsSize = activeDbIdsLength; var activeDbIdsSnapshot = activeDbIds; - while(currIdx < activeDbIdsSize) + while (currIdx < activeDbIdsSize) { var dbId = activeDbIdsSnapshot[currIdx]; tasks[currIdx] = CheckpointDatabaseTask(dbId, storeType, logger); @@ -1650,7 +1649,7 @@ private void HandleDatabaseAdded(int dbId) // Set an updated instance of activeDbIds var activeDbIdsSnapshot = activeDbIds; var activeDbIdsUpdated = new int[newSize]; - + if (activeDbIdsSnapshot != null) { Array.Copy(activeDbIdsSnapshot, activeDbIdsUpdated, dbIdIdx); diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index 17f0f10011..50aa346d39 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -1,14 +1,14 @@ -using System.Linq; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using Garnet.common; using NUnit.Framework; using NUnit.Framework.Legacy; using StackExchange.Redis; -using Garnet.common; namespace Garnet.test { @@ -21,7 +21,7 @@ public class MultiDatabaseTests public void Setup() { TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); - server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, enableAOF: true, lowMemory:true); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, enableAOF: true, lowMemory: true); server.Start(); } @@ -99,7 +99,7 @@ public void MultiDatabaseSameKeyTestSE() ClassicAssert.AreEqual("db1:val1", db1val.ToString()); var db2val = db2.SetMembers(key1); - CollectionAssert.AreEquivalent(db2val, new[] {new RedisValue("db2:val1"), new RedisValue("db2:val2")}); + CollectionAssert.AreEquivalent(db2val, new[] { new RedisValue("db2:val1"), new RedisValue("db2:val2") }); var db12val = db12.ListLeftPop(key1); ClassicAssert.AreEqual("db12:val2", db12val.ToString()); @@ -441,8 +441,8 @@ public void MultiDatabaseAofRecoverObjectTest() { var db1Key = "db1:key1"; var db2Key = "db2:key1"; - var db1data = new SortedSetEntry[]{ new("db1:a", 1), new("db1:b", 2), new("db1:c", 3) }; - var db2data = new SortedSetEntry[]{ new("db2:a", -1), new("db2:b", -2), new("db2:c", -3) }; + var db1data = new SortedSetEntry[] { new("db1:a", 1), new("db1:b", 2), new("db1:c", 3) }; + var db2data = new SortedSetEntry[] { new("db2:a", -1), new("db2:b", -2), new("db2:c", -3) }; using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig())) { @@ -506,4 +506,4 @@ public void TearDown() return data; } } -} +} \ No newline at end of file From 27016357da203dcb481ab09cc16805a87ddefb15 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 6 Feb 2025 19:09:34 -0800 Subject: [PATCH 20/82] small fix --- libs/host/GarnetServer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/host/GarnetServer.cs b/libs/host/GarnetServer.cs index ffdb139d3f..298c4acbdd 100644 --- a/libs/host/GarnetServer.cs +++ b/libs/host/GarnetServer.cs @@ -19,7 +19,6 @@ namespace Garnet { - using static System.Formats.Asn1.AsnWriter; using MainStoreAllocator = SpanByteAllocator>; using MainStoreFunctions = StoreFunctions; From 84a30bc513c995c02e1b3d907c161612ed2263e0 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Fri, 7 Feb 2025 22:16:29 -0800 Subject: [PATCH 21/82] wip --- test/Garnet.test/MultiDatabaseTests.cs | 54 +++++++++++++++++++------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index 50aa346d39..1ae8bcd282 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -190,6 +190,11 @@ public void MultiDatabaseSelectMultithreadedTestSE() var tuples = GenerateDataset(dbCount, keyCount); using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var dbConnections = new IDatabase[dbCount]; + for (var i = 0; i < dbCount; i++) + { + dbConnections[i] = redis.GetDatabase(i); + } // In parallel, add each (key, value) pair to a database of id db-id var kvBag = new ConcurrentBag<(int, string, string)>(tuples); @@ -199,16 +204,17 @@ public void MultiDatabaseSelectMultithreadedTestSE() tasks[i] = Task.Run(() => { kvBag.TryTake(out var tup); - var db = redis.GetDatabase(tup.Item1); + var db = dbConnections[tup.Item1]; return db.StringSet(tup.Item2, tup.Item3); }); } // Wait for all tasks to finish - Task.WaitAll(tasks); + var completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(10)); + ClassicAssert.IsTrue(completed); // Check that all tasks successfully entered the data to the respective database - Assert.That(tasks, Has.All.Matches>(t => t.Result)); + Assert.That(tasks, Has.All.Matches>(t => t.IsCompletedSuccessfully && t.Result)); // In parallel, retrieve the actual value for each db-id and key kvBag = new(tuples); @@ -217,14 +223,16 @@ public void MultiDatabaseSelectMultithreadedTestSE() tasks[i] = Task.Run(() => { kvBag.TryTake(out var tup); - var db = redis.GetDatabase(tup.Item1); + var db = dbConnections[tup.Item1]; var actualValue = db.StringGet(tup.Item2); return (tup.Item1, tup.Item2, actualValue.ToString()); }); } // Wait for all tasks to finish - Task.WaitAll(tasks); + completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(10)); + ClassicAssert.IsTrue(completed); + ClassicAssert.IsTrue(tasks.All(t => t.IsCompletedSuccessfully)); // Check that (db-id, key, actual-value) tuples match original (db-id, key, value) tuples var results = tasks.Select(t => ((Task<(int, string, string)>)t).Result); @@ -256,14 +264,24 @@ public void MultiDatabaseSelectMultithreadedTestLC() kvBag.TryTake(out var tup); var expectedResponse = "+OK\r\n+OK\r\n"; var lcRequest = lcRequests.Take(); - var response = lcRequest.Execute($"SELECT {tup.Item1}", $"SET {tup.Item2} {tup.Item3}", expectedResponse.Length); - lcRequests.Add(lcRequest); - return expectedResponse == response; + string response; + try + { + response = lcRequest.Execute($"SELECT {tup.Item1}", $"SET {tup.Item2} {tup.Item3}", expectedResponse.Length); + } + finally + { + lcRequests.Add(lcRequest); + } + + return response != null && expectedResponse == response; }); } // Wait for all tasks to finish - Task.WaitAll(tasks); + var completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(10)); + ClassicAssert.IsTrue(completed); + ClassicAssert.IsTrue(tasks.All(t => t.IsCompletedSuccessfully)); // Check that all tasks successfully entered the data to the respective database Assert.That(tasks, Has.All.Matches>(t => t.Result)); @@ -277,14 +295,24 @@ public void MultiDatabaseSelectMultithreadedTestLC() kvBag.TryTake(out var tup); var expectedResponse = $"+OK\r\n${tup.Item3.Length}\r\n{tup.Item3}\r\n"; var lcRequest = lcRequests.Take(); - var response = lcRequest.Execute($"SELECT {tup.Item1}", $"GET {tup.Item2}", expectedResponse.Length); - lcRequests.Add(lcRequest); - return expectedResponse == response; + string response; + try + { + response = lcRequest.Execute($"SELECT {tup.Item1}", $"GET {tup.Item2}", expectedResponse.Length); + } + finally + { + lcRequests.Add(lcRequest); + } + + return response != null && expectedResponse == response; }); } // Wait for all tasks to finish - Task.WaitAll(tasks); + completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(10)); + ClassicAssert.IsTrue(completed); + ClassicAssert.IsTrue(tasks.All(t => t.IsCompletedSuccessfully)); // Check that all the tasks retrieved the correct value successfully Assert.That(tasks, Has.All.Matches>(t => t.Result)); From ee68ec595074b1506af1bd15f03b6c480a6fbb33 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Fri, 7 Feb 2025 23:14:57 -0800 Subject: [PATCH 22/82] test --- test/Garnet.test/MultiDatabaseTests.cs | 32 +++++++++++++++++++------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index 1ae8bcd282..c3ee02923f 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -197,13 +197,17 @@ public void MultiDatabaseSelectMultithreadedTestSE() } // In parallel, add each (key, value) pair to a database of id db-id - var kvBag = new ConcurrentBag<(int, string, string)>(tuples); + var kvCollection = new BlockingCollection<(int, string, string)>(); + foreach (var t in tuples) + kvCollection.Add(t); + kvCollection.CompleteAdding(); + var tasks = new Task[dbCount * keyCount]; for (var i = 0; i < tasks.Length; i++) { tasks[i] = Task.Run(() => { - kvBag.TryTake(out var tup); + var tup = kvCollection.Take(); var db = dbConnections[tup.Item1]; return db.StringSet(tup.Item2, tup.Item3); }); @@ -217,12 +221,16 @@ public void MultiDatabaseSelectMultithreadedTestSE() Assert.That(tasks, Has.All.Matches>(t => t.IsCompletedSuccessfully && t.Result)); // In parallel, retrieve the actual value for each db-id and key - kvBag = new(tuples); + kvCollection = new BlockingCollection<(int, string, string)>(); + foreach (var t in tuples) + kvCollection.Add(t); + kvCollection.CompleteAdding(); + for (var i = 0; i < tasks.Length; i++) { tasks[i] = Task.Run(() => { - kvBag.TryTake(out var tup); + var tup = kvCollection.Take(); var db = dbConnections[tup.Item1]; var actualValue = db.StringGet(tup.Item2); return (tup.Item1, tup.Item2, actualValue.ToString()); @@ -255,13 +263,17 @@ public void MultiDatabaseSelectMultithreadedTestLC() } // In parallel, add each (key, value) pair to a database of id db-id - var kvBag = new ConcurrentBag<(int, string, string)>(tuples); + var kvCollection = new BlockingCollection<(int, string, string)>(); + foreach (var t in tuples) + kvCollection.Add(t); + kvCollection.CompleteAdding(); + var tasks = new Task[dbCount * keyCount]; for (var i = 0; i < tasks.Length; i++) { tasks[i] = Task.Run(() => { - kvBag.TryTake(out var tup); + var tup = kvCollection.Take(); var expectedResponse = "+OK\r\n+OK\r\n"; var lcRequest = lcRequests.Take(); string response; @@ -287,12 +299,16 @@ public void MultiDatabaseSelectMultithreadedTestLC() Assert.That(tasks, Has.All.Matches>(t => t.Result)); // In parallel, retrieve the actual value for each db-id and key - kvBag = new(tuples); + kvCollection = new BlockingCollection<(int, string, string)>(); + foreach (var t in tuples) + kvCollection.Add(t); + kvCollection.CompleteAdding(); + for (var i = 0; i < tasks.Length; i++) { tasks[i] = Task.Run(() => { - kvBag.TryTake(out var tup); + var tup = kvCollection.Take(); var expectedResponse = $"+OK\r\n${tup.Item3.Length}\r\n{tup.Item3}\r\n"; var lcRequest = lcRequests.Take(); string response; From 535203eb670ab4431df810d1d16aedabb57d40e2 Mon Sep 17 00:00:00 2001 From: Vasileios Zois Date: Mon, 10 Feb 2025 12:30:15 -0800 Subject: [PATCH 23/82] simplify dispose logic for gcs at AofSyncTask --- .../Replication/PrimaryOps/AofSyncTaskInfo.cs | 21 +------------------ libs/server/StoreWrapper.cs | 2 +- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/libs/cluster/Server/Replication/PrimaryOps/AofSyncTaskInfo.cs b/libs/cluster/Server/Replication/PrimaryOps/AofSyncTaskInfo.cs index 7065ece0e4..611fdc5077 100644 --- a/libs/cluster/Server/Replication/PrimaryOps/AofSyncTaskInfo.cs +++ b/libs/cluster/Server/Replication/PrimaryOps/AofSyncTaskInfo.cs @@ -5,7 +5,6 @@ using System.Threading; using System.Threading.Tasks; using Garnet.client; -using Garnet.common; using Microsoft.Extensions.Logging; using Tsavorite.core; @@ -24,11 +23,6 @@ internal sealed class AofSyncTaskInfo : IBulkLogEntryConsumer, IDisposable readonly long startAddress; public long previousAddress; - /// - /// Used to mark if syncing is in progress - /// - SingleWriterMultiReaderLock aofSyncInProgress; - /// /// Check if client connection is healthy /// @@ -69,11 +63,6 @@ public void Dispose() // Finally, dispose the cts cts?.Dispose(); - - // Dispose only if AOF sync has not started - // otherwise sync task will dispose the client - if (aofSyncInProgress.TryWriteLock()) - garnetClient?.Dispose(); } public unsafe void Consume(byte* payloadPtr, int payloadLength, long currentAddress, long nextAddress, bool isProtected) @@ -108,16 +97,8 @@ public async Task ReplicaSyncTask() { logger?.LogInformation("Starting ReplicationManager.ReplicaSyncTask for remote node {remoteNodeId} starting from address {address}", remoteNodeId, startAddress); - var failedToStart = false; try { - if (!aofSyncInProgress.TryWriteLock()) - { - logger?.LogWarning("{method} AOF sync for {remoteNodeId} failed to start", nameof(ReplicaSyncTask), remoteNodeId); - failedToStart = true; - return; - } - if (!IsConnected) garnetClient.Connect(); iter = clusterProvider.storeWrapper.appendOnlyFile.ScanSingle(startAddress, long.MaxValue, scanUncommitted: true, recover: false, logger: logger); @@ -134,7 +115,7 @@ public async Task ReplicaSyncTask() } finally { - if (!failedToStart) garnetClient.Dispose(); + garnetClient.Dispose(); var (address, port) = clusterProvider.clusterManager.CurrentConfig.GetWorkerAddressFromNodeId(remoteNodeId); logger?.LogWarning("AofSync task terminated; client disposed {remoteNodeId} {address} {port} {currentAddress}", remoteNodeId, address, port, previousAddress); diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 964d46b4fc..919a5e51c2 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -576,7 +576,7 @@ async Task AutoCheckpointBasedOnAofSizeLimit(long aofSizeLimit, CancellationToke await Task.Delay(1000, token); if (token.IsCancellationRequested) break; - var aofSizeAtLimit = -1l; + var aofSizeAtLimit = -1L; var activeDbIdsSize = activeDbIdsLength; if (!allowMultiDb || activeDbIdsSize == 1) From 6e5645eca1dcbde65cfb9b7ef1d4a1730b3b7be7 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Mon, 10 Feb 2025 13:33:29 -0800 Subject: [PATCH 24/82] fix --- libs/server/StoreWrapper.cs | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 37297741dd..c1b0994e0c 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -175,6 +175,9 @@ public sealed class StoreWrapper // Stream provider for serializing DB IDs file readonly IStreamProvider databaseIdsStreamProvider; + // True if StoreWrapper instance is disposed + bool disposed = false; + /// /// Constructor /// @@ -1176,6 +1179,9 @@ private bool GrowIndexIfNeeded(StoreType storeType, long indexMaxSize, long over /// public void Dispose() { + if (disposed) return; + disposed = true; + // Wait for checkpoints to complete and disable checkpointing checkpointTaskLock.WriteLock(); @@ -1217,8 +1223,14 @@ public void ResumeCheckpoints() public async Task TakeOnDemandCheckpoint(DateTimeOffset entryTime, int dbId = 0) { // Take lock to ensure no other task will be taking a checkpoint - while (!TryPauseCheckpoints()) + var lockAcquired = TryPauseCheckpoints(); + while (!lockAcquired && !disposed) + { await Task.Yield(); + lockAcquired = TryPauseCheckpoints(); + } + + if (disposed) return; // If an external task has taken a checkpoint beyond the provided entryTime return if (databases.Map[dbId].LastSaveTime > entryTime) @@ -1268,16 +1280,22 @@ public bool TakeCheckpoint(bool background, StoreType storeType = StoreType.All, /// ID of database to checkpoint (default: DB 0) /// True if lock previously acquired /// Logger + /// Cancellation token /// Task - private async Task CheckpointTask(StoreType storeType, int dbId = 0, bool lockAcquired = false, ILogger logger = null) + private async Task CheckpointTask(StoreType storeType, int dbId = 0, bool lockAcquired = false, ILogger logger = null, CancellationToken token = default) { try { if (!lockAcquired) { // Take lock to ensure no other task will be taking a checkpoint - while (!TryPauseCheckpoints()) + while (!lockAcquired && !token.IsCancellationRequested) + { await Task.Yield(); + lockAcquired = TryPauseCheckpoints(); + } + + if (token.IsCancellationRequested) return; } await CheckpointDatabaseTask(dbId, storeType, logger); @@ -1291,7 +1309,8 @@ private async Task CheckpointTask(StoreType storeType, int dbId = 0, bool lockAc } finally { - ResumeCheckpoints(); + if (lockAcquired) + ResumeCheckpoints(); } } From aeddd717cd8595ce3b9363e21bfa9fa5982f3019 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Mon, 10 Feb 2025 13:46:06 -0800 Subject: [PATCH 25/82] format --- libs/server/StoreWrapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index c1b0994e0c..41363fcbfd 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -1309,7 +1309,7 @@ private async Task CheckpointTask(StoreType storeType, int dbId = 0, bool lockAc } finally { - if (lockAcquired) + if (lockAcquired) ResumeCheckpoints(); } } From 78251c6f48205b9dd4db97bb8fef9ca9876c6612 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Mon, 10 Feb 2025 16:15:58 -0800 Subject: [PATCH 26/82] fix --- libs/server/StoreWrapper.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 41363fcbfd..dc66e30873 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -591,7 +591,7 @@ async Task AutoCheckpointBasedOnAofSizeLimit(long aofSizeLimit, CancellationToke if (aofSizeAtLimit != -1) { logger?.LogInformation("Enforcing AOF size limit currentAofSize: {currAofSize} > AofSizeLimit: {AofSizeLimit}", aofSizeAtLimit, aofSizeLimit); - await CheckpointTask(StoreType.All, logger: logger); + await CheckpointTask(StoreType.All, logger: logger, token: token); } return; @@ -1259,7 +1259,7 @@ public bool TakeCheckpoint(bool background, StoreType storeType = StoreType.All, if (!allowMultiDb || activeDbIdsSize == 1 || dbId != -1) { if (dbId == -1) dbId = 0; - var checkpointTask = Task.Run(async () => await CheckpointTask(storeType, dbId, logger: logger), token); + var checkpointTask = Task.Run(async () => await CheckpointTask(storeType, dbId, logger: logger, token: token), token); if (background) return true; @@ -1286,18 +1286,15 @@ private async Task CheckpointTask(StoreType storeType, int dbId = 0, bool lockAc { try { - if (!lockAcquired) + // Take lock to ensure no other task will be taking a checkpoint + while (!lockAcquired && !token.IsCancellationRequested && !disposed) { - // Take lock to ensure no other task will be taking a checkpoint - while (!lockAcquired && !token.IsCancellationRequested) - { - await Task.Yield(); - lockAcquired = TryPauseCheckpoints(); - } - - if (token.IsCancellationRequested) return; + await Task.Yield(); + lockAcquired = TryPauseCheckpoints(); } + if (token.IsCancellationRequested || disposed) return; + await CheckpointDatabaseTask(dbId, storeType, logger); if (checkpointDatabaseIdsPath != null) From 2c82c2d9fefe0959be0554d7289e0e29a1600d9a Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Mon, 10 Feb 2025 16:38:15 -0800 Subject: [PATCH 27/82] wip --- libs/server/StoreWrapper.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index dc66e30873..d175734a17 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -1180,7 +1180,6 @@ private bool GrowIndexIfNeeded(StoreType storeType, long indexMaxSize, long over public void Dispose() { if (disposed) return; - disposed = true; // Wait for checkpoints to complete and disable checkpointing checkpointTaskLock.WriteLock(); @@ -1199,6 +1198,8 @@ public void Dispose() ctsCommit?.Dispose(); clusterProvider?.Dispose(); + + disposed = true; } /// @@ -1230,12 +1231,11 @@ public async Task TakeOnDemandCheckpoint(DateTimeOffset entryTime, int dbId = 0) lockAcquired = TryPauseCheckpoints(); } - if (disposed) return; - // If an external task has taken a checkpoint beyond the provided entryTime return - if (databases.Map[dbId].LastSaveTime > entryTime) + if (disposed || databases.Map[dbId].LastSaveTime > entryTime) { - ResumeCheckpoints(); + if (lockAcquired) + ResumeCheckpoints(); return; } From 989e74e5b5330988b63a05960d321459a2ae843e Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Mon, 10 Feb 2025 17:28:05 -0800 Subject: [PATCH 28/82] test --- libs/server/StoreWrapper.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index d175734a17..4ea0b4761e 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -1180,6 +1180,7 @@ private bool GrowIndexIfNeeded(StoreType storeType, long indexMaxSize, long over public void Dispose() { if (disposed) return; + disposed = true; // Wait for checkpoints to complete and disable checkpointing checkpointTaskLock.WriteLock(); @@ -1198,8 +1199,6 @@ public void Dispose() ctsCommit?.Dispose(); clusterProvider?.Dispose(); - - disposed = true; } /// From 3fb53be3c20b3de1dbf1823077240a94f92e7970 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Mon, 10 Feb 2025 20:11:19 -0800 Subject: [PATCH 29/82] readding multithreading tests --- test/Garnet.test/MultiDatabaseTests.cs | 64 +++++++++----------------- 1 file changed, 23 insertions(+), 41 deletions(-) diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index 047a40481b..800e1e2328 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -182,7 +182,6 @@ public void MultiDatabaseBasicSelectTestLC() } [Test] - [Ignore("")] public void MultiDatabaseSelectMultithreadedTestSE() { // Create a set of tuples (db-id, key, value) @@ -198,19 +197,14 @@ public void MultiDatabaseSelectMultithreadedTestSE() } // In parallel, add each (key, value) pair to a database of id db-id - var kvCollection = new BlockingCollection<(int, string, string)>(); - foreach (var t in tuples) - kvCollection.Add(t); - kvCollection.CompleteAdding(); - - var tasks = new Task[dbCount * keyCount]; + var tasks = new Task[tuples.Length]; for (var i = 0; i < tasks.Length; i++) { + var tup = tuples[i]; tasks[i] = Task.Run(() => { - var tup = kvCollection.Take(); var db = dbConnections[tup.Item1]; - return db.StringSet(tup.Item2, tup.Item3); + return db.StringSet(tup.Item3, tup.Item4); }); } @@ -222,36 +216,30 @@ public void MultiDatabaseSelectMultithreadedTestSE() Assert.That(tasks, Has.All.Matches>(t => t.IsCompletedSuccessfully && t.Result)); // In parallel, retrieve the actual value for each db-id and key - kvCollection = new BlockingCollection<(int, string, string)>(); - foreach (var t in tuples) - kvCollection.Add(t); - kvCollection.CompleteAdding(); - for (var i = 0; i < tasks.Length; i++) { + var tup = tuples[i]; tasks[i] = Task.Run(() => { - var tup = kvCollection.Take(); var db = dbConnections[tup.Item1]; - var actualValue = db.StringGet(tup.Item2); - return (tup.Item1, tup.Item2, actualValue.ToString()); + var actualValue = db.StringGet(tup.Item3); + return actualValue.ToString() == tup.Item4; }); } // Wait for all tasks to finish completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(10)); ClassicAssert.IsTrue(completed); - ClassicAssert.IsTrue(tasks.All(t => t.IsCompletedSuccessfully)); // Check that (db-id, key, actual-value) tuples match original (db-id, key, value) tuples - var results = tasks.Select(t => ((Task<(int, string, string)>)t).Result); - Assert.That(results, Is.EquivalentTo(tuples)); + Assert.That(tasks, Has.All.Matches>(t => t.IsCompletedSuccessfully && t.Result)); } [Test] - [Ignore("")] public void MultiDatabaseSelectMultithreadedTestLC() { + var cts = new CancellationTokenSource(); + // Create a set of tuples (db-id, key, value) var dbCount = 16; var keyCount = 16; @@ -265,19 +253,16 @@ public void MultiDatabaseSelectMultithreadedTestLC() } // In parallel, add each (key, value) pair to a database of id db-id - var kvCollection = new BlockingCollection<(int, string, string)>(); - foreach (var t in tuples) - kvCollection.Add(t); - kvCollection.CompleteAdding(); - var tasks = new Task[dbCount * keyCount]; for (var i = 0; i < tasks.Length; i++) { + var tup = tuples[i]; tasks[i] = Task.Run(() => { - var tup = kvCollection.Take(); var expectedResponse = "+OK\r\n+OK\r\n"; - var lcRequest = lcRequests.Take(); + var lcRequest = lcRequests.Take(cts.Token); + if (cts.Token.IsCancellationRequested) return false; + string response; try { @@ -295,24 +280,21 @@ public void MultiDatabaseSelectMultithreadedTestLC() // Wait for all tasks to finish var completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(10)); ClassicAssert.IsTrue(completed); - ClassicAssert.IsTrue(tasks.All(t => t.IsCompletedSuccessfully)); + cts.Cancel(); + cts = new CancellationTokenSource(); // Check that all tasks successfully entered the data to the respective database - Assert.That(tasks, Has.All.Matches>(t => t.Result)); + Assert.That(tasks, Has.All.Matches>(t => t.IsCompletedSuccessfully && t.Result)); // In parallel, retrieve the actual value for each db-id and key - kvCollection = new BlockingCollection<(int, string, string)>(); - foreach (var t in tuples) - kvCollection.Add(t); - kvCollection.CompleteAdding(); for (var i = 0; i < tasks.Length; i++) { + var tup = tuples[i]; tasks[i] = Task.Run(() => { - var tup = kvCollection.Take(); var expectedResponse = $"+OK\r\n${tup.Item3.Length}\r\n{tup.Item3}\r\n"; - var lcRequest = lcRequests.Take(); + var lcRequest = lcRequests.Take(cts.Token); string response; try { @@ -330,10 +312,10 @@ public void MultiDatabaseSelectMultithreadedTestLC() // Wait for all tasks to finish completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(10)); ClassicAssert.IsTrue(completed); - ClassicAssert.IsTrue(tasks.All(t => t.IsCompletedSuccessfully)); + cts.Cancel(); // Check that all the tasks retrieved the correct value successfully - Assert.That(tasks, Has.All.Matches>(t => t.Result)); + Assert.That(tasks, Has.All.Matches>(t => t.IsCompletedSuccessfully && t.Result)); while (lcRequests.TryTake(out var client)) client.Dispose(); @@ -537,15 +519,15 @@ public void TearDown() TestUtils.DeleteDirectory(TestUtils.MethodTestDir); } - private List<(int, string, string)> GenerateDataset(int dbCount, int keyCount) + private (int, int, string, string)[] GenerateDataset(int dbCount, int keyCount) { - var data = new List<(int, string, string)>(); + var data = new (int, int, string, string)[dbCount * keyCount]; for (var dbId = 0; dbId < dbCount; dbId++) { for (var keyId = 0; keyId < keyCount; keyId++) { - data.Add((dbId, $"key{keyId}", $"db{dbId}:val{keyId}")); + data[(keyCount * dbId) + keyId] = (dbId, keyId, $"key{keyId}", $"db{dbId}:val{keyId}"); } } From a9d8f617db0353ee3c2d09f5541fa54f9f6e8a36 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Mon, 10 Feb 2025 20:21:28 -0800 Subject: [PATCH 30/82] test --- test/Garnet.test/MultiDatabaseTests.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index 800e1e2328..a510a57f25 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; @@ -184,6 +185,7 @@ public void MultiDatabaseBasicSelectTestLC() [Test] public void MultiDatabaseSelectMultithreadedTestSE() { + Debug.WriteLine("MultiDatabaseSelectMultithreadedTestSE"); // Create a set of tuples (db-id, key, value) var dbCount = 16; var keyCount = 16; @@ -211,7 +213,7 @@ public void MultiDatabaseSelectMultithreadedTestSE() // Wait for all tasks to finish var completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(10)); ClassicAssert.IsTrue(completed); - + Debug.WriteLine("Inserted items"); // Check that all tasks successfully entered the data to the respective database Assert.That(tasks, Has.All.Matches>(t => t.IsCompletedSuccessfully && t.Result)); @@ -230,6 +232,7 @@ public void MultiDatabaseSelectMultithreadedTestSE() // Wait for all tasks to finish completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(10)); ClassicAssert.IsTrue(completed); + Debug.WriteLine("Retrieved items"); // Check that (db-id, key, actual-value) tuples match original (db-id, key, value) tuples Assert.That(tasks, Has.All.Matches>(t => t.IsCompletedSuccessfully && t.Result)); @@ -238,6 +241,7 @@ public void MultiDatabaseSelectMultithreadedTestSE() [Test] public void MultiDatabaseSelectMultithreadedTestLC() { + Debug.WriteLine("MultiDatabaseSelectMultithreadedTestLC"); var cts = new CancellationTokenSource(); // Create a set of tuples (db-id, key, value) @@ -279,6 +283,7 @@ public void MultiDatabaseSelectMultithreadedTestLC() // Wait for all tasks to finish var completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(10)); + Debug.WriteLine("Inserted items"); ClassicAssert.IsTrue(completed); cts.Cancel(); cts = new CancellationTokenSource(); @@ -312,6 +317,7 @@ public void MultiDatabaseSelectMultithreadedTestLC() // Wait for all tasks to finish completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(10)); ClassicAssert.IsTrue(completed); + Debug.WriteLine("Retrieved items"); cts.Cancel(); // Check that all the tasks retrieved the correct value successfully From 44de3864e227579ee0ca85a911a7320060e1bd67 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Mon, 10 Feb 2025 20:43:49 -0800 Subject: [PATCH 31/82] test --- test/Garnet.test/MultiDatabaseTests.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index a510a57f25..2adeddf332 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -185,7 +185,7 @@ public void MultiDatabaseBasicSelectTestLC() [Test] public void MultiDatabaseSelectMultithreadedTestSE() { - Debug.WriteLine("MultiDatabaseSelectMultithreadedTestSE"); + Console.WriteLine("MultiDatabaseSelectMultithreadedTestSE"); // Create a set of tuples (db-id, key, value) var dbCount = 16; var keyCount = 16; @@ -211,9 +211,9 @@ public void MultiDatabaseSelectMultithreadedTestSE() } // Wait for all tasks to finish - var completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(10)); + var completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(60)); ClassicAssert.IsTrue(completed); - Debug.WriteLine("Inserted items"); + Console.WriteLine("Inserted items"); // Check that all tasks successfully entered the data to the respective database Assert.That(tasks, Has.All.Matches>(t => t.IsCompletedSuccessfully && t.Result)); @@ -230,9 +230,9 @@ public void MultiDatabaseSelectMultithreadedTestSE() } // Wait for all tasks to finish - completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(10)); + completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(60)); ClassicAssert.IsTrue(completed); - Debug.WriteLine("Retrieved items"); + Console.WriteLine("Retrieved items"); // Check that (db-id, key, actual-value) tuples match original (db-id, key, value) tuples Assert.That(tasks, Has.All.Matches>(t => t.IsCompletedSuccessfully && t.Result)); @@ -241,7 +241,7 @@ public void MultiDatabaseSelectMultithreadedTestSE() [Test] public void MultiDatabaseSelectMultithreadedTestLC() { - Debug.WriteLine("MultiDatabaseSelectMultithreadedTestLC"); + Console.WriteLine("MultiDatabaseSelectMultithreadedTestLC"); var cts = new CancellationTokenSource(); // Create a set of tuples (db-id, key, value) @@ -282,8 +282,8 @@ public void MultiDatabaseSelectMultithreadedTestLC() } // Wait for all tasks to finish - var completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(10)); - Debug.WriteLine("Inserted items"); + var completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(60)); + Console.WriteLine("Inserted items"); ClassicAssert.IsTrue(completed); cts.Cancel(); cts = new CancellationTokenSource(); @@ -315,9 +315,9 @@ public void MultiDatabaseSelectMultithreadedTestLC() } // Wait for all tasks to finish - completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(10)); + completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(60)); ClassicAssert.IsTrue(completed); - Debug.WriteLine("Retrieved items"); + Console.WriteLine("Retrieved items"); cts.Cancel(); // Check that all the tasks retrieved the correct value successfully From 096424076ffd2f023cd02546eb52a73e384ab851 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Mon, 10 Feb 2025 22:05:40 -0800 Subject: [PATCH 32/82] wip --- test/Garnet.test/MultiDatabaseTests.cs | 52 +++++++++++++------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index 2adeddf332..a3735fd59a 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -185,10 +185,9 @@ public void MultiDatabaseBasicSelectTestLC() [Test] public void MultiDatabaseSelectMultithreadedTestSE() { - Console.WriteLine("MultiDatabaseSelectMultithreadedTestSE"); // Create a set of tuples (db-id, key, value) var dbCount = 16; - var keyCount = 16; + var keyCount = 8; var tuples = GenerateDataset(dbCount, keyCount); using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); @@ -203,17 +202,17 @@ public void MultiDatabaseSelectMultithreadedTestSE() for (var i = 0; i < tasks.Length; i++) { var tup = tuples[i]; - tasks[i] = Task.Run(() => + tasks[i] = Task.Run(async () => { var db = dbConnections[tup.Item1]; - return db.StringSet(tup.Item3, tup.Item4); + return await db.StringSetAsync(tup.Item3, tup.Item4).ConfigureAwait(false); }); } // Wait for all tasks to finish - var completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(60)); - ClassicAssert.IsTrue(completed); - Console.WriteLine("Inserted items"); + if (!Task.WhenAll(tasks).Wait(TimeSpan.FromSeconds(60))) + Assert.Fail("Items not inserted in allotted time."); + // Check that all tasks successfully entered the data to the respective database Assert.That(tasks, Has.All.Matches>(t => t.IsCompletedSuccessfully && t.Result)); @@ -221,18 +220,17 @@ public void MultiDatabaseSelectMultithreadedTestSE() for (var i = 0; i < tasks.Length; i++) { var tup = tuples[i]; - tasks[i] = Task.Run(() => + tasks[i] = Task.Run(async () => { var db = dbConnections[tup.Item1]; - var actualValue = db.StringGet(tup.Item3); + var actualValue = await db.StringGetAsync(tup.Item3).ConfigureAwait(false); return actualValue.ToString() == tup.Item4; }); } // Wait for all tasks to finish - completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(60)); - ClassicAssert.IsTrue(completed); - Console.WriteLine("Retrieved items"); + if (!Task.WhenAll(tasks).Wait(TimeSpan.FromSeconds(60))) + Assert.Fail("Items not retrieved in allotted time."); // Check that (db-id, key, actual-value) tuples match original (db-id, key, value) tuples Assert.That(tasks, Has.All.Matches>(t => t.IsCompletedSuccessfully && t.Result)); @@ -241,12 +239,11 @@ public void MultiDatabaseSelectMultithreadedTestSE() [Test] public void MultiDatabaseSelectMultithreadedTestLC() { - Console.WriteLine("MultiDatabaseSelectMultithreadedTestLC"); var cts = new CancellationTokenSource(); // Create a set of tuples (db-id, key, value) var dbCount = 16; - var keyCount = 16; + var keyCount = 8; var tuples = GenerateDataset(dbCount, keyCount); // Create multiple LC request objects to be used @@ -274,18 +271,20 @@ public void MultiDatabaseSelectMultithreadedTestLC() } finally { - lcRequests.Add(lcRequest); + lcRequests.Add(lcRequest, cts.Token); } return response != null && expectedResponse == response; - }); + }, cts.Token); } // Wait for all tasks to finish - var completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(60)); - Console.WriteLine("Inserted items"); - ClassicAssert.IsTrue(completed); - cts.Cancel(); + if (!Task.WhenAll(tasks).Wait(TimeSpan.FromSeconds(60))) + { + cts.Cancel(); + Assert.Fail("Items not inserted in allotted time."); + } + cts = new CancellationTokenSource(); // Check that all tasks successfully entered the data to the respective database @@ -307,18 +306,19 @@ public void MultiDatabaseSelectMultithreadedTestLC() } finally { - lcRequests.Add(lcRequest); + lcRequests.Add(lcRequest, cts.Token); } return response != null && expectedResponse == response; - }); + }, cts.Token); } // Wait for all tasks to finish - completed = Task.WaitAll(tasks, TimeSpan.FromSeconds(60)); - ClassicAssert.IsTrue(completed); - Console.WriteLine("Retrieved items"); - cts.Cancel(); + if (!Task.WhenAll(tasks).Wait(TimeSpan.FromSeconds(60))) + { + cts.Cancel(); + Assert.Fail("Items not retrieved in allotted time."); + } // Check that all the tasks retrieved the correct value successfully Assert.That(tasks, Has.All.Matches>(t => t.IsCompletedSuccessfully && t.Result)); From cc6dfa8a7570372d537c967a0b284bfa7693bf45 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 11 Feb 2025 14:29:19 -0800 Subject: [PATCH 33/82] test --- test/Garnet.test/MultiDatabaseTests.cs | 112 +++++++++++++------------ 1 file changed, 58 insertions(+), 54 deletions(-) diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index a3735fd59a..3bd4d3a0e9 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -182,60 +182,6 @@ public void MultiDatabaseBasicSelectTestLC() ClassicAssert.AreEqual(expectedResponse, actualValue); } - [Test] - public void MultiDatabaseSelectMultithreadedTestSE() - { - // Create a set of tuples (db-id, key, value) - var dbCount = 16; - var keyCount = 8; - var tuples = GenerateDataset(dbCount, keyCount); - - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var dbConnections = new IDatabase[dbCount]; - for (var i = 0; i < dbCount; i++) - { - dbConnections[i] = redis.GetDatabase(i); - } - - // In parallel, add each (key, value) pair to a database of id db-id - var tasks = new Task[tuples.Length]; - for (var i = 0; i < tasks.Length; i++) - { - var tup = tuples[i]; - tasks[i] = Task.Run(async () => - { - var db = dbConnections[tup.Item1]; - return await db.StringSetAsync(tup.Item3, tup.Item4).ConfigureAwait(false); - }); - } - - // Wait for all tasks to finish - if (!Task.WhenAll(tasks).Wait(TimeSpan.FromSeconds(60))) - Assert.Fail("Items not inserted in allotted time."); - - // Check that all tasks successfully entered the data to the respective database - Assert.That(tasks, Has.All.Matches>(t => t.IsCompletedSuccessfully && t.Result)); - - // In parallel, retrieve the actual value for each db-id and key - for (var i = 0; i < tasks.Length; i++) - { - var tup = tuples[i]; - tasks[i] = Task.Run(async () => - { - var db = dbConnections[tup.Item1]; - var actualValue = await db.StringGetAsync(tup.Item3).ConfigureAwait(false); - return actualValue.ToString() == tup.Item4; - }); - } - - // Wait for all tasks to finish - if (!Task.WhenAll(tasks).Wait(TimeSpan.FromSeconds(60))) - Assert.Fail("Items not retrieved in allotted time."); - - // Check that (db-id, key, actual-value) tuples match original (db-id, key, value) tuples - Assert.That(tasks, Has.All.Matches>(t => t.IsCompletedSuccessfully && t.Result)); - } - [Test] public void MultiDatabaseSelectMultithreadedTestLC() { @@ -327,6 +273,64 @@ public void MultiDatabaseSelectMultithreadedTestLC() client.Dispose(); } + [Test] + public void MultiDatabaseSelectMultithreadedTestSE() + { + // Create a set of tuples (db-id, key, value) + var dbCount = 16; + var keyCount = 8; + var tuples = GenerateDataset(dbCount, keyCount); + + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var dbConnections = new IDatabase[dbCount]; + for (var i = 0; i < dbCount; i++) + { + dbConnections[i] = redis.GetDatabase(i); + } + + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + + // In parallel, add each (key, value) pair to a database of id db-id + var tasks = new Task[tuples.Length]; + for (var i = 0; i < tasks.Length; i++) + { + var tup = tuples[i]; + tasks[i] = Task.Run(async () => + { + var db = dbConnections[tup.Item1]; + return await db.StringSetAsync(tup.Item3, tup.Item4).ConfigureAwait(false); + }, cts.Token); + } + + // Wait for all tasks to finish + if (!Task.WhenAll(tasks).Wait(TimeSpan.FromSeconds(60), cts.Token)) + Assert.Fail("Items not inserted in allotted time."); + + // Check that all tasks successfully entered the data to the respective database + Assert.That(tasks, Has.All.Matches>(t => t.IsCompletedSuccessfully && t.Result)); + + cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + + // In parallel, retrieve the actual value for each db-id and key + for (var i = 0; i < tasks.Length; i++) + { + var tup = tuples[i]; + tasks[i] = Task.Run(async () => + { + var db = dbConnections[tup.Item1]; + var actualValue = await db.StringGetAsync(tup.Item3).ConfigureAwait(false); + return actualValue.ToString() == tup.Item4; + }, cts.Token); + } + + // Wait for all tasks to finish + if (!Task.WhenAll(tasks).Wait(TimeSpan.FromSeconds(60), cts.Token)) + Assert.Fail("Items not retrieved in allotted time."); + + // Check that (db-id, key, actual-value) tuples match original (db-id, key, value) tuples + Assert.That(tasks, Has.All.Matches>(t => t.IsCompletedSuccessfully && t.Result)); + } + [Test] public void MultiDatabaseSaveRecoverObjectTest() { From 2a40dfa43f57797bbc6d3b4d6d68c8b9a038ccb4 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 11 Feb 2025 14:55:32 -0800 Subject: [PATCH 34/82] format --- test/Garnet.test/MultiDatabaseTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index 3bd4d3a0e9..72a3ffc1cb 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -310,7 +310,7 @@ public void MultiDatabaseSelectMultithreadedTestSE() Assert.That(tasks, Has.All.Matches>(t => t.IsCompletedSuccessfully && t.Result)); cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - + // In parallel, retrieve the actual value for each db-id and key for (var i = 0; i < tasks.Length; i++) { From de48e833c9cb9baeba8a0aef93ee73ba8637aec9 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 11 Feb 2025 16:21:03 -0800 Subject: [PATCH 35/82] wip --- test/Garnet.test/MultiDatabaseTests.cs | 73 ++++++++++++-------------- 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index 72a3ffc1cb..8dda03bfd0 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -193,34 +193,31 @@ public void MultiDatabaseSelectMultithreadedTestLC() var tuples = GenerateDataset(dbCount, keyCount); // Create multiple LC request objects to be used - var lcRequests = new BlockingCollection(); - for (var i = 0; i < 16; i++) - { - lcRequests.Add(TestUtils.CreateRequest(countResponseType: CountResponseType.Bytes)); - } + var lcRequests = new LightClientRequest[16]; + for (var i = 0; i < lcRequests.Length; i++) + lcRequests[i] = TestUtils.CreateRequest(countResponseType: CountResponseType.Bytes); // In parallel, add each (key, value) pair to a database of id db-id - var tasks = new Task[dbCount * keyCount]; + var tasks = new Task[lcRequests.Length]; + var results = new bool[tuples.Length]; + var tupIdx = -1; for (var i = 0; i < tasks.Length; i++) { - var tup = tuples[i]; + var lcRequest = lcRequests[i]; tasks[i] = Task.Run(() => { - var expectedResponse = "+OK\r\n+OK\r\n"; - var lcRequest = lcRequests.Take(cts.Token); - if (cts.Token.IsCancellationRequested) return false; - - string response; - try - { - response = lcRequest.Execute($"SELECT {tup.Item1}", $"SET {tup.Item2} {tup.Item3}", expectedResponse.Length); - } - finally + while (true) { - lcRequests.Add(lcRequest, cts.Token); - } + var currTupIdx = Interlocked.Increment(ref tupIdx); + if (currTupIdx >= tuples.Length) break; + + var tup = tuples[currTupIdx]; + + var expectedResponse = "+OK\r\n+OK\r\n"; + var response = lcRequest.Execute($"SELECT {tup.Item1}", $"SET {tup.Item2} {tup.Item3}", expectedResponse.Length); - return response != null && expectedResponse == response; + results[currTupIdx] = response != null && expectedResponse == response; + } }, cts.Token); } @@ -231,31 +228,32 @@ public void MultiDatabaseSelectMultithreadedTestLC() Assert.Fail("Items not inserted in allotted time."); } - cts = new CancellationTokenSource(); - // Check that all tasks successfully entered the data to the respective database - Assert.That(tasks, Has.All.Matches>(t => t.IsCompletedSuccessfully && t.Result)); + Assert.That(results, Is.All.True); - // In parallel, retrieve the actual value for each db-id and key + cts = new CancellationTokenSource(); + // In parallel, retrieve the actual value for each db-id and key for (var i = 0; i < tasks.Length; i++) { - var tup = tuples[i]; + var lcRequest = lcRequests[i]; tasks[i] = Task.Run(() => { - var expectedResponse = $"+OK\r\n${tup.Item3.Length}\r\n{tup.Item3}\r\n"; - var lcRequest = lcRequests.Take(cts.Token); - string response; - try - { - response = lcRequest.Execute($"SELECT {tup.Item1}", $"GET {tup.Item2}", expectedResponse.Length); - } - finally + while (true) { - lcRequests.Add(lcRequest, cts.Token); + var currTupIdx = Interlocked.Increment(ref tupIdx); + if (currTupIdx >= tuples.Length) break; + + var tup = tuples[currTupIdx]; + + var expectedResponse = $"+OK\r\n${tup.Item3.Length}\r\n{tup.Item3}\r\n"; + var response = lcRequest.Execute($"SELECT {tup.Item1}", $"GET {tup.Item2}", + expectedResponse.Length); + + results[currTupIdx] = response != null && expectedResponse == response; } - return response != null && expectedResponse == response; + lcRequest.Dispose(); }, cts.Token); } @@ -267,10 +265,7 @@ public void MultiDatabaseSelectMultithreadedTestLC() } // Check that all the tasks retrieved the correct value successfully - Assert.That(tasks, Has.All.Matches>(t => t.IsCompletedSuccessfully && t.Result)); - - while (lcRequests.TryTake(out var client)) - client.Dispose(); + Assert.That(results, Is.All.True); } [Test] From d2ff6e335e2be13ff446cae59f69ef4dd38b2f7a Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 11 Feb 2025 16:58:50 -0800 Subject: [PATCH 36/82] Added FLUSHALL + tests --- libs/server/Resp/BasicCommands.cs | 19 +++++--- libs/server/StoreWrapper.cs | 29 ++++++++++++ test/Garnet.test/MultiDatabaseTests.cs | 61 ++++++++++++++++++++++++-- 3 files changed, 99 insertions(+), 10 deletions(-) diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index b6c719d03f..8fed869d44 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -1656,21 +1656,26 @@ void FlushDb(RespCommand cmd) } if (async) - Task.Run(() => ExecuteFlushDb(unsafeTruncateLog)).ConfigureAwait(false); + Task.Run(() => ExecuteFlushDb(cmd, unsafeTruncateLog)).ConfigureAwait(false); else - ExecuteFlushDb(unsafeTruncateLog); + ExecuteFlushDb(cmd, unsafeTruncateLog); logger?.LogInformation($"Running {nameof(cmd)} {{async}} {{mode}}", async ? "async" : "sync", unsafeTruncateLog ? " with unsafetruncatelog." : string.Empty); while (!RespWriteUtils.TryWriteDirect(CmdStrings.RESP_OK, ref dcurr, dend)) SendAndReset(); } - void ExecuteFlushDb(bool unsafeTruncateLog) + void ExecuteFlushDb(RespCommand cmd, bool unsafeTruncateLog) { - var dbFound = storeWrapper.TryGetDatabase(activeDbId, out var db); - Debug.Assert(dbFound); - db.MainStore.Log.ShiftBeginAddress(db.MainStore.Log.TailAddress, truncateLog: unsafeTruncateLog); - db.ObjectStore?.Log.ShiftBeginAddress(db.ObjectStore.Log.TailAddress, truncateLog: unsafeTruncateLog); + switch (cmd) + { + case RespCommand.FLUSHDB: + storeWrapper.FlushDatabase(unsafeTruncateLog, activeDbId); + break; + case RespCommand.FLUSHALL: + storeWrapper.FlushAllDatabases(unsafeTruncateLog); + break; + } } /// diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 4ea0b4761e..d88d4df20a 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -1547,6 +1547,35 @@ public bool TryGetDatabase(int dbId, out GarnetDatabase db) return this.TryGetOrAddDatabase(dbId, out db); } + /// + /// Flush database with specified ID + /// + /// Truncate log + /// Database ID + public void FlushDatabase(bool unsafeTruncateLog, int dbId = 0) + { + var dbFound = TryGetDatabase(dbId, out var db); + Debug.Assert(dbFound); + + db.MainStore.Log.ShiftBeginAddress(db.MainStore.Log.TailAddress, truncateLog: unsafeTruncateLog); + db.ObjectStore?.Log.ShiftBeginAddress(db.ObjectStore.Log.TailAddress, truncateLog: unsafeTruncateLog); + } + + /// + /// Flush all active databases + /// + /// Truncate log + public void FlushAllDatabases(bool unsafeTruncateLog) + { + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + for (var i = 0; i < activeDbIdsSize; i++) + { + var dbId = activeDbIdsSnapshot[i]; + this.FlushDatabase(unsafeTruncateLog, dbId); + } + } + /// /// Copy active databases from specified StoreWrapper instance /// diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index 8dda03bfd0..c0f92b8d66 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; @@ -106,6 +103,64 @@ public void MultiDatabaseSameKeyTestSE() ClassicAssert.AreEqual("db12:val2", db12val.ToString()); } + [Test] + public void MultiDatabaseFlushDatabasesTestSE() + { + var db1Key1 = "db1:key1"; + var db1Key2 = "db1:key2"; + var db2Key1 = "db2:key1"; + var db2Key2 = "db2:key2"; + var db12Key1 = "db12:key1"; + var db12Key2 = "db12:key2"; + var db1data = new RedisValue[] { "db1:a", "db1:b", "db1:c", "db1:d" }; + var db2data = new RedisValue[] { "db2:a", "db2:b", "db2:c", "db2:d" }; + var db12data = new RedisValue[] { "db12:a", "db12:b", "db12:c", "db12:d" }; + + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + + var db1 = redis.GetDatabase(0); + var result = db1.StringSet(db1Key1, "db1:val1"); + ClassicAssert.IsTrue(result); + + var count = db1.ListLeftPush(db1Key2, db1data); + ClassicAssert.AreEqual(db1data.Length, count); + + var db2 = redis.GetDatabase(1); + result = db2.StringSet(db2Key1, "db2:val1"); + ClassicAssert.IsTrue(result); + + count = db2.ListLeftPush(db2Key2, db2data); + ClassicAssert.AreEqual(db2data.Length, count); + + var db12 = redis.GetDatabase(11); + result = db12.StringSet(db12Key1, "db12:val1"); + ClassicAssert.IsTrue(result); + + count = db12.ListLeftPush(db12Key2, db12data); + ClassicAssert.AreEqual(db12data.Length, count); + + var opResult = db1.Execute("FLUSHDB"); + ClassicAssert.AreEqual("OK", opResult.ToString()); + + ClassicAssert.IsFalse(db1.KeyExists(db1Key1)); + ClassicAssert.IsFalse(db1.KeyExists(db1Key2)); + + ClassicAssert.IsTrue(db2.KeyExists(db2Key1)); + ClassicAssert.IsTrue(db2.KeyExists(db2Key2)); + + ClassicAssert.IsTrue(db12.KeyExists(db12Key1)); + ClassicAssert.IsTrue(db12.KeyExists(db12Key2)); + + opResult = db1.Execute("FLUSHALL"); + ClassicAssert.AreEqual("OK", opResult.ToString()); + + ClassicAssert.IsFalse(db2.KeyExists(db2Key1)); + ClassicAssert.IsFalse(db2.KeyExists(db2Key2)); + + ClassicAssert.IsFalse(db12.KeyExists(db12Key1)); + ClassicAssert.IsFalse(db12.KeyExists(db12Key2)); + } + [Test] public void MultiDatabaseBasicSelectTestLC() { From 64e610af9960af351c804ee3074d3a7dc5e31de1 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 11 Feb 2025 17:00:55 -0800 Subject: [PATCH 37/82] Ignore LC MT test --- test/Garnet.test/MultiDatabaseTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index c0f92b8d66..2bab6d5d67 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -238,6 +238,7 @@ public void MultiDatabaseBasicSelectTestLC() } [Test] + [Ignore("")] public void MultiDatabaseSelectMultithreadedTestLC() { var cts = new CancellationTokenSource(); From 6782af6f00f1e62362b1179f65356a517c860a3d Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 11 Feb 2025 17:06:25 -0800 Subject: [PATCH 38/82] format --- test/Garnet.test/MultiDatabaseTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index 2bab6d5d67..94d0bbc305 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -117,7 +117,7 @@ public void MultiDatabaseFlushDatabasesTestSE() var db12data = new RedisValue[] { "db12:a", "db12:b", "db12:c", "db12:d" }; using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - + var db1 = redis.GetDatabase(0); var result = db1.StringSet(db1Key1, "db1:val1"); ClassicAssert.IsTrue(result); From d0bf7f6efa2d34f457b9c6c5069c665743f958ef Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 11 Feb 2025 21:40:29 -0800 Subject: [PATCH 39/82] Added DB ID to client info --- libs/server/Resp/BasicCommands.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index 8fed869d44..4cf9d63a64 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -1691,6 +1691,7 @@ private static void WriteClientInfo(IClusterProvider provider, StringBuilder int var localEndpoint = targetSession.networkSender.LocalEndpointName; var clientName = targetSession.clientName; var user = targetSession._user; + var db = targetSession.activeDbId; var resp = targetSession.respProtocolVersion; var nodeId = targetSession?.clusterSession?.RemoteNodeId; @@ -1734,6 +1735,7 @@ private static void WriteClientInfo(IClusterProvider provider, StringBuilder int } } + into.Append($" db={db}"); into.Append($" resp={resp}"); into.Append($" lib-name={targetSession.clientLibName}"); into.Append($" lib-ver={targetSession.clientLibVersion}"); From 0532ede14f34a5bb8260251bd7909825976043c7 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 11 Feb 2025 22:01:12 -0800 Subject: [PATCH 40/82] SAVE, BGSAVE, LASTSAVE with ID - not tested yet --- libs/server/Resp/AdminCommands.cs | 76 +++++++++++++++++++++++++++++-- libs/server/Resp/CmdStrings.cs | 1 + 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/libs/server/Resp/AdminCommands.cs b/libs/server/Resp/AdminCommands.cs index 33f00ee0b5..ad6fe9c2bb 100644 --- a/libs/server/Resp/AdminCommands.cs +++ b/libs/server/Resp/AdminCommands.cs @@ -729,6 +729,10 @@ private bool NetworkROLE() return true; } + /// + /// SAVE [DBID] + /// + /// private bool NetworkSAVE() { if (parseState.Count > 1) @@ -739,7 +743,27 @@ private bool NetworkSAVE() var dbId = -1; if (parseState.Count == 1) { - parseState.TryGetInt(0, out dbId); + if (!parseState.TryGetInt(0, out dbId)) + { + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref dcurr, dend)) + SendAndReset(); + return true; + } + + if (dbId != 0 && storeWrapper.serverOptions.EnableCluster) + { + // Cluster mode does not allow DBID + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_SELECT_CLUSTER_MODE, ref dcurr, dend)) + SendAndReset(); + return true; + } + + if (dbId >= storeWrapper.serverOptions.MaxDatabases) + { + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_SELECT_INVALID_INDEX, ref dcurr, dend)) + SendAndReset(); + return true; + } } if (!storeWrapper.TakeCheckpoint(false, dbId: dbId, logger: logger)) @@ -756,6 +780,10 @@ private bool NetworkSAVE() return true; } + /// + /// LASTSAVE + /// + /// private bool NetworkLASTSAVE() { if (parseState.Count != 0) @@ -763,21 +791,61 @@ private bool NetworkLASTSAVE() return AbortWithWrongNumberOfArguments(nameof(RespCommand.SAVE)); } - var seconds = storeWrapper.lastSaveTime.ToUnixTimeSeconds(); + storeWrapper.TryGetDatabase(activeDbId, out var db); + + var seconds = db.LastSaveTime.ToUnixTimeSeconds(); while (!RespWriteUtils.TryWriteInt64(seconds, ref dcurr, dend)) SendAndReset(); return true; } + /// + /// BGSAVE [SCHEDULE] [DBID] + /// + /// private bool NetworkBGSAVE() { - if (parseState.Count > 1) + if (parseState.Count > 2) { return AbortWithWrongNumberOfArguments(nameof(RespCommand.BGSAVE)); } - var success = storeWrapper.TakeCheckpoint(true, StoreType.All, logger: logger); + var dbId = -1; + var tokenIdx = 0; + if (parseState.Count > 0) + { + if (parseState.GetArgSliceByRef(tokenIdx).ReadOnlySpan + .EqualsUpperCaseSpanIgnoringCase(CmdStrings.SCHEDULE)) + tokenIdx++; + + if (parseState.Count - tokenIdx > 0) + { + if (!parseState.TryGetInt(tokenIdx, out dbId)) + { + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref dcurr, dend)) + SendAndReset(); + return true; + } + + if (dbId != 0 && storeWrapper.serverOptions.EnableCluster) + { + // Cluster mode does not allow DBID + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_SELECT_CLUSTER_MODE, ref dcurr, dend)) + SendAndReset(); + return true; + } + + if (dbId >= storeWrapper.serverOptions.MaxDatabases) + { + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_SELECT_INVALID_INDEX, ref dcurr, dend)) + SendAndReset(); + return true; + } + } + } + + var success = storeWrapper.TakeCheckpoint(true, dbId: dbId, logger: logger); if (success) { while (!RespWriteUtils.TryWriteSimpleString("Background saving started"u8, ref dcurr, dend)) diff --git a/libs/server/Resp/CmdStrings.cs b/libs/server/Resp/CmdStrings.cs index f8d48830ad..60bbcb4011 100644 --- a/libs/server/Resp/CmdStrings.cs +++ b/libs/server/Resp/CmdStrings.cs @@ -145,6 +145,7 @@ static partial class CmdStrings public static ReadOnlySpan FIELDS => "FIELDS"u8; public static ReadOnlySpan TIMEOUT => "TIMEOUT"u8; public static ReadOnlySpan ERROR => "ERROR"u8; + public static ReadOnlySpan SCHEDULE => "SCHEDULE"u8; /// /// Response strings From ca47b0062116ae7189f8897999bf0ca00fb359b7 Mon Sep 17 00:00:00 2001 From: Vasileios Zois Date: Wed, 12 Feb 2025 14:50:50 -0800 Subject: [PATCH 41/82] ensure waitForSync before dispose --- .../ReplicationTests/ClusterReplicationBaseTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/Garnet.test.cluster/ReplicationTests/ClusterReplicationBaseTests.cs b/test/Garnet.test.cluster/ReplicationTests/ClusterReplicationBaseTests.cs index 4f7749a8a9..30d4f29c21 100644 --- a/test/Garnet.test.cluster/ReplicationTests/ClusterReplicationBaseTests.cs +++ b/test/Garnet.test.cluster/ReplicationTests/ClusterReplicationBaseTests.cs @@ -754,6 +754,8 @@ public void ClusterReplicationCheckpointCleanupTest([Values] bool performRMW, [V if (!attachReplicaTask.Wait(TimeSpan.FromSeconds(30))) Assert.Fail("attachReplicaTask timeout"); + + context.clusterTestUtils.WaitForReplicaAofSync(primaryIndex: 0, secondaryIndex: 1, logger: context.logger); } [Test, Order(14)] From e881dbc245830eaba723d0275dc81e6a11d119ea Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Wed, 12 Feb 2025 15:47:21 -0800 Subject: [PATCH 42/82] wip --- libs/server/Resp/AdminCommands.cs | 2 +- test/Garnet.test/MultiDatabaseTests.cs | 64 +++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/libs/server/Resp/AdminCommands.cs b/libs/server/Resp/AdminCommands.cs index ad6fe9c2bb..66ff7e3519 100644 --- a/libs/server/Resp/AdminCommands.cs +++ b/libs/server/Resp/AdminCommands.cs @@ -788,7 +788,7 @@ private bool NetworkLASTSAVE() { if (parseState.Count != 0) { - return AbortWithWrongNumberOfArguments(nameof(RespCommand.SAVE)); + return AbortWithWrongNumberOfArguments(nameof(RespCommand.LASTSAVE)); } storeWrapper.TryGetDatabase(activeDbId, out var db); diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index 94d0bbc305..c071592c51 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -435,7 +435,7 @@ public void MultiDatabaseSaveRecoverObjectTest() public void MultiDatabaseSaveRecoverRawStringTest() { var db1Key = "db1:key1"; - var db2Key = "db1:key1"; + var db2Key = "db2:key1"; var db1data = new RedisValue("db1:a"); var db2data = new RedisValue("db2:a"); RedisValue db1DataBeforeRecovery; @@ -527,6 +527,68 @@ public void MultiDatabaseAofRecoverRawStringTest() [Test] public void MultiDatabaseAofRecoverObjectTest() + { + var db1Key1 = "db1:key1"; + var db1Key2 = "db1:key2"; + var db2Key1 = "db2:key1"; + var db2Key2 = "db2:key2"; + var db1val = new RedisValue("db1:a"); + var db2val = new RedisValue("db2:a"); + var db1data = new SortedSetEntry[] { new("db1:a", 1), new("db1:b", 2), new("db1:c", 3) }; + var db2data = new SortedSetEntry[] { new("db2:a", -1), new("db2:b", -2), new("db2:c", -3) }; + + using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig())) + { + var db1 = redis.GetDatabase(0); + var added = db1.SortedSetAdd(db1Key1, db1data); + ClassicAssert.AreEqual(3, added); + + var set = db1.StringSet(db1Key2, db1val); + ClassicAssert.IsTrue(set); + + var db2 = redis.GetDatabase(1); + added = db2.SortedSetAdd(db2Key1, db2data); + ClassicAssert.AreEqual(3, added); + + set = db1.StringSet(db2Key2, db2val); + ClassicAssert.IsTrue(set); + + // Issue DB SAVE for db2 + var res = db1.Execute("SAVE", "1"); + ClassicAssert.AreEqual("OK", res.ToString()); + var lastSaveStr = db2.Execute("LASTSAVE").ToString(); + var parsed = long.TryParse(lastSaveStr, out var lastSave); + ClassicAssert.IsTrue(parsed); + ClassicAssert.AreEqual(0, lastSave); + + lastSaveStr = db1.Execute("LASTSAVE").ToString(); + parsed = long.TryParse(lastSaveStr, out lastSave); + ClassicAssert.IsTrue(parsed); + ClassicAssert.AreEqual(0, lastSave); + } + + server.Dispose(false); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, tryRecover: true); + server.Start(); + + using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig())) + { + var db1 = redis.GetDatabase(0); + + var score = db1.SortedSetScore(db1Key1, "db1:a"); + ClassicAssert.IsTrue(score.HasValue); + ClassicAssert.AreEqual(1, score.Value); + + var db2 = redis.GetDatabase(1); + + score = db2.SortedSetScore(db2Key1, "db2:a"); + ClassicAssert.IsTrue(score.HasValue); + ClassicAssert.AreEqual(-1, score.Value); + } + } + + [Test] + public void MultiDatabaseSaveDatabaseTest() { var db1Key = "db1:key1"; var db2Key = "db2:key1"; From 0cc431a90898eb98eecf526337788ec60866d4ac Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 13 Feb 2025 16:15:46 -0800 Subject: [PATCH 43/82] wip --- libs/host/GarnetServer.cs | 4 +- libs/server/StoreWrapper.cs | 189 +++++++++++++------------ test/Garnet.test/MultiDatabaseTests.cs | 95 +++++++------ 3 files changed, 152 insertions(+), 136 deletions(-) diff --git a/libs/host/GarnetServer.cs b/libs/host/GarnetServer.cs index d477f9f7c8..a330f28eeb 100644 --- a/libs/host/GarnetServer.cs +++ b/libs/host/GarnetServer.cs @@ -317,9 +317,9 @@ private TsavoriteKV kvSettings.CheckpointVersionSwitchBarrier = opts.EnableCluster; var checkpointFactory = opts.DeviceFactoryCreator(); - mainStoreCheckpointDir = Path.Combine(checkpointDir, "Store"); - var baseName = Path.Combine(mainStoreCheckpointDir, $"checkpoints{(dbId == 0 ? string.Empty : $"_{dbId}")}"); + var baseName = Path.Combine(checkpointDir, "Store", $"checkpoints{(dbId == 0 ? string.Empty : $"_{dbId}")}"); var defaultNamingScheme = new DefaultCheckpointNamingScheme(baseName); + mainStoreCheckpointDir = baseName; kvSettings.CheckpointManager = opts.EnableCluster ? clusterFactory.CreateCheckpointManager(checkpointFactory, defaultNamingScheme, isMainStore: true, logger) : diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index d88d4df20a..a5d735f776 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -5,6 +5,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Net; using System.Net.Sockets; @@ -163,14 +164,15 @@ public sealed class StoreWrapper Task[] aofTasks; readonly object activeDbIdsLock = new(); - // File name of serialized binary file containing all DB IDs - const string DatabaseIdsFileName = "dbIds.dat"; - // Path of serialization for the DB IDs file used when committing / recovering to / from AOF - readonly string aofDatabaseIdsPath; + readonly string aofParentDir; + + readonly string aofDirBaseName; // Path of serialization for the DB IDs file used when committing / recovering to / from a checkpoint - readonly string checkpointDatabaseIdsPath; + readonly string checkpointParentDir; + + readonly string checkpointDirBaseName; // Stream provider for serializing DB IDs file readonly IStreamProvider databaseIdsStreamProvider; @@ -223,9 +225,18 @@ public StoreWrapper( // Create default database of index 0 (unless specified otherwise) if (createDefaultDatabase) { - var db = createDatabasesDelegate(0, out var storeCheckpointDir, out var aofPath); - checkpointDatabaseIdsPath = storeCheckpointDir == null ? null : Path.Combine(storeCheckpointDir, DatabaseIdsFileName); - aofDatabaseIdsPath = aofPath == null ? null : Path.Combine(aofPath, DatabaseIdsFileName); + var db = createDatabasesDelegate(0, out var storeCheckpointDir, out var aofDir); + + var checkpointDirInfo = new DirectoryInfo(storeCheckpointDir); + this.checkpointDirBaseName = checkpointDirInfo.Name; + this.checkpointParentDir = checkpointDirInfo.Parent!.FullName; + + if (aofDir != null) + { + var aofDirInfo = new DirectoryInfo(aofDir); + this.aofDirBaseName = aofDirInfo.Name; + this.aofParentDir = aofDirInfo.Parent!.FullName; + } // Set new database in map if (!this.TryAddDatabase(0, ref db)) @@ -400,25 +411,17 @@ public void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStore } else { - if (allowMultiDb && checkpointDatabaseIdsPath != null) - RecoverDatabases(checkpointDatabaseIdsPath); - - var databasesMapSnapshot = databases.Map; - - var activeDbIdsSize = activeDbIdsLength; - var activeDbIdsSnapshot = activeDbIds; - - for (var i = 0; i < activeDbIdsSize; i++) + if (!allowMultiDb) { - var dbId = activeDbIdsSnapshot[i]; - var db = databasesMapSnapshot[dbId]; - storeVersion = db.MainStore.Recover(); - if (db.ObjectStore != null) objectStoreVersion = db.ObjectStore.Recover(); + RecoverDatabaseCheckpoint(0); + } + else + { + if (!TryGetSavedDatabaseIds(checkpointParentDir, checkpointDirBaseName, out var dbIdsToRecover)) + return; - var lastSave = DateTimeOffset.UtcNow; - db.LastSaveTime = lastSave; - if (dbId == 0 && (storeVersion > 0 || objectStoreVersion > 0)) - lastSaveTime = lastSave; + foreach (var dbId in dbIdsToRecover) + RecoverDatabaseCheckpoint(dbId); } } } @@ -435,30 +438,56 @@ public void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStore } } + private void RecoverDatabaseCheckpoint(int dbId) + { + long storeVersion = -1, objectStoreVersion = -1; + + var success = TryGetOrAddDatabase(dbId, out var db); + Debug.Assert(success); + + storeVersion = db.MainStore.Recover(); + if (db.ObjectStore != null) objectStoreVersion = db.ObjectStore.Recover(); + + if (storeVersion > 0 || objectStoreVersion > 0) + { + var lastSave = DateTimeOffset.UtcNow; + db.LastSaveTime = lastSave; + if (dbId == 0) + lastSaveTime = lastSave; + + var databasesMapSnapshot = databases.Map; + databasesMapSnapshot[dbId] = db; + } + } + /// /// Recover AOF /// public void RecoverAOF() { - if (allowMultiDb && aofDatabaseIdsPath != null) - RecoverDatabases(aofDatabaseIdsPath); - - var databasesMapSnapshot = databases.Map; + if (!allowMultiDb) + { + RecoverDatabaseAOF(0); + } + else if (aofParentDir != null) + { + if (!TryGetSavedDatabaseIds(aofParentDir, aofDirBaseName, out var dbIdsToRecover)) + return; - var activeDbIdsSize = activeDbIdsLength; - var activeDbIdsSnapshot = activeDbIds; + foreach (var dbId in dbIdsToRecover) + RecoverDatabaseAOF(dbId); + } + } - for (var i = 0; i < activeDbIdsSize; i++) - { - var dbId = activeDbIdsSnapshot[i]; - var db = databasesMapSnapshot[dbId]; - Debug.Assert(!db.IsDefault()); + private void RecoverDatabaseAOF(int dbId) + { + var success = TryGetOrAddDatabase(dbId, out var db); + Debug.Assert(success); - if (db.AppendOnlyFile == null) continue; + if (db.AppendOnlyFile == null) return; - db.AppendOnlyFile.Recover(); - logger?.LogInformation("Recovered AOF: begin address = {beginAddress}, tail address = {tailAddress}", db.AppendOnlyFile.BeginAddress, db.AppendOnlyFile.TailAddress); - } + db.AppendOnlyFile.Recover(); + logger?.LogInformation("Recovered AOF: begin address = {beginAddress}, tail address = {tailAddress}", db.AppendOnlyFile.BeginAddress, db.AppendOnlyFile.TailAddress); } /// @@ -691,16 +720,10 @@ void MultiDatabaseCommit(ref Task[] tasks, CancellationToken token, bool spinWai tasks[i] = db.AppendOnlyFile.CommitAsync(null, token).AsTask(); } - var completion = Task.WhenAll(tasks).ContinueWith(_ => - { - if (aofDatabaseIdsPath != null) - WriteDatabaseIdsSnapshot(aofDatabaseIdsPath); - }, token); - if (!spinWait) return; - completion.Wait(token); + Task.WhenAll(tasks).Wait(token); } async Task CompactionTask(int compactionFrequencySecs, CancellationToken token = default) @@ -1295,9 +1318,6 @@ private async Task CheckpointTask(StoreType storeType, int dbId = 0, bool lockAc if (token.IsCancellationRequested || disposed) return; await CheckpointDatabaseTask(dbId, storeType, logger); - - if (checkpointDatabaseIdsPath != null) - WriteDatabaseIdsSnapshot(checkpointDatabaseIdsPath); } catch (Exception ex) { @@ -1351,9 +1371,6 @@ private bool CheckpointDatabases(StoreType storeType, ref int[] dbIds, ref Task[ for (var i = 0; i < currIdx; i++) tasks[i].Wait(token); - - if (checkpointDatabaseIdsPath != null) - WriteDatabaseIdsSnapshot(checkpointDatabaseIdsPath); } catch (Exception ex) { @@ -1409,6 +1426,7 @@ private async Task CheckpointDatabaseTask(int dbId, StoreType storeType, ILogger if (dbId == 0) lastSaveTime = lastSave; db.LastSaveTime = lastSave; + databasesMapSnapshot[dbId] = db; } private async Task InitiateCheckpoint(int dbId, GarnetDatabase db, bool full, CheckpointType checkpointType, bool tryIncremental, StoreType storeType, ILogger logger = null) @@ -1614,48 +1632,41 @@ private void InitializeFieldsFromDatabase(GarnetDatabase db) } /// - /// Serializes an array of active DB IDs to file (excluding DB 0) + /// Retrieves saved database IDs from parent checkpoint / AOF path + /// e.g. if path contains directories: baseName, baseName_1, baseName_2, baseName_10 + /// DB IDs 0,1,2,10 will be returned /// - /// Path of serialized file - private void WriteDatabaseIdsSnapshot(string path) + /// Parent path + /// Base name of directories containing database-specific checkpoints / AOFs + /// DB IDs extracted from parent path + /// True if successful + private bool TryGetSavedDatabaseIds(string path, string baseName, out int[] dbIds) { - var activeDbIdsSize = activeDbIdsLength; - var activeDbIdsSnapshot = activeDbIds; - - var dbIdsWithLength = new int[activeDbIdsSize]; - dbIdsWithLength[0] = activeDbIdsSize - 1; - Array.Copy(activeDbIdsSnapshot, 1, dbIdsWithLength, 1, activeDbIdsSize - 1); - - var dbIdData = new byte[sizeof(int) * dbIdsWithLength.Length]; - - Buffer.BlockCopy(dbIdsWithLength, 0, dbIdData, 0, dbIdData.Length); - databaseIdsStreamProvider.Write(path, dbIdData); - } - - /// - /// Recover databases from serialized DB IDs file - /// - /// Path of serialized file - private void RecoverDatabases(string path) - { - using var stream = databaseIdsStreamProvider.Read(path); - using var streamReader = new BinaryReader(stream); + dbIds = default; + if (!Directory.Exists(path)) return false; - if (streamReader.BaseStream.Length > 0) + try { - // Read length - var idsCount = streamReader.ReadInt32(); - - // Read DB IDs - var dbIds = new int[idsCount]; - var dbIdData = streamReader.ReadBytes((int)streamReader.BaseStream.Length); - Buffer.BlockCopy(dbIdData, 0, dbIds, 0, dbIdData.Length); - - // Add databases - foreach (var dbId in dbIds) + var dirs = Directory.GetDirectories(path, $"{baseName}*", SearchOption.TopDirectoryOnly); + dbIds = new int[dirs.Length]; + for (var i = 0; i < dirs.Length; i++) { - this.TryGetOrAddDatabase(dbId, out _); + var dirName = new DirectoryInfo(dirs[i]).Name; + var sepIdx = dirName.IndexOf('_'); + var dbId = 0; + + if (sepIdx != -1 && !int.TryParse(dirName.AsSpan(sepIdx + 1), out dbId)) + continue; + + dbIds[i] = dbId; } + + return true; + } + catch (Exception e) + { + logger?.LogError(e, "Encountered an error while trying to parse save databases IDs."); + return false; } } diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index c071592c51..c3cef4b15b 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Garnet.common; +using Newtonsoft.Json.Linq; using NUnit.Framework; using NUnit.Framework.Legacy; using StackExchange.Redis; @@ -528,110 +529,114 @@ public void MultiDatabaseAofRecoverRawStringTest() [Test] public void MultiDatabaseAofRecoverObjectTest() { - var db1Key1 = "db1:key1"; - var db1Key2 = "db1:key2"; - var db2Key1 = "db2:key1"; - var db2Key2 = "db2:key2"; - var db1val = new RedisValue("db1:a"); - var db2val = new RedisValue("db2:a"); + var db1Key = "db1:key1"; + var db2Key = "db2:key1"; var db1data = new SortedSetEntry[] { new("db1:a", 1), new("db1:b", 2), new("db1:c", 3) }; var db2data = new SortedSetEntry[] { new("db2:a", -1), new("db2:b", -2), new("db2:c", -3) }; using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig())) { var db1 = redis.GetDatabase(0); - var added = db1.SortedSetAdd(db1Key1, db1data); + var added = db1.SortedSetAdd(db1Key, db1data); ClassicAssert.AreEqual(3, added); - var set = db1.StringSet(db1Key2, db1val); - ClassicAssert.IsTrue(set); + var score = db1.SortedSetScore(db1Key, "db1:a"); + ClassicAssert.IsTrue(score.HasValue); + ClassicAssert.AreEqual(1, score.Value); var db2 = redis.GetDatabase(1); - added = db2.SortedSetAdd(db2Key1, db2data); + added = db2.SortedSetAdd(db2Key, db2data); ClassicAssert.AreEqual(3, added); - set = db1.StringSet(db2Key2, db2val); - ClassicAssert.IsTrue(set); - - // Issue DB SAVE for db2 - var res = db1.Execute("SAVE", "1"); - ClassicAssert.AreEqual("OK", res.ToString()); - var lastSaveStr = db2.Execute("LASTSAVE").ToString(); - var parsed = long.TryParse(lastSaveStr, out var lastSave); - ClassicAssert.IsTrue(parsed); - ClassicAssert.AreEqual(0, lastSave); - - lastSaveStr = db1.Execute("LASTSAVE").ToString(); - parsed = long.TryParse(lastSaveStr, out lastSave); - ClassicAssert.IsTrue(parsed); - ClassicAssert.AreEqual(0, lastSave); + score = db2.SortedSetScore(db2Key, "db2:a"); + ClassicAssert.IsTrue(score.HasValue); + ClassicAssert.AreEqual(-1, score.Value); } + server.Store.CommitAOF(true); server.Dispose(false); - server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, tryRecover: true); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, tryRecover: true, enableAOF: true); server.Start(); using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig())) { var db1 = redis.GetDatabase(0); - var score = db1.SortedSetScore(db1Key1, "db1:a"); + var score = db1.SortedSetScore(db1Key, "db1:a"); ClassicAssert.IsTrue(score.HasValue); ClassicAssert.AreEqual(1, score.Value); var db2 = redis.GetDatabase(1); - score = db2.SortedSetScore(db2Key1, "db2:a"); + score = db2.SortedSetScore(db2Key, "db2:a"); ClassicAssert.IsTrue(score.HasValue); ClassicAssert.AreEqual(-1, score.Value); } } [Test] - public void MultiDatabaseSaveDatabaseTest() + public void MultiDatabaseSaveByIdTest() { - var db1Key = "db1:key1"; - var db2Key = "db2:key1"; + var db1Key1 = "db1:key1"; + var db1Key2 = "db1:key2"; + var db2Key1 = "db2:key1"; + var db2Key2 = "db2:key2"; + var db1val = new RedisValue("db1:a"); + var db2val = new RedisValue("db2:a"); var db1data = new SortedSetEntry[] { new("db1:a", 1), new("db1:b", 2), new("db1:c", 3) }; var db2data = new SortedSetEntry[] { new("db2:a", -1), new("db2:b", -2), new("db2:c", -3) }; using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig())) { var db1 = redis.GetDatabase(0); - var added = db1.SortedSetAdd(db1Key, db1data); + var added = db1.SortedSetAdd(db1Key1, db1data); ClassicAssert.AreEqual(3, added); - var score = db1.SortedSetScore(db1Key, "db1:a"); - ClassicAssert.IsTrue(score.HasValue); - ClassicAssert.AreEqual(1, score.Value); + var set = db1.StringSet(db1Key2, db1val); + ClassicAssert.IsTrue(set); var db2 = redis.GetDatabase(1); - added = db2.SortedSetAdd(db2Key, db2data); + added = db2.SortedSetAdd(db2Key1, db2data); ClassicAssert.AreEqual(3, added); - score = db2.SortedSetScore(db2Key, "db2:a"); - ClassicAssert.IsTrue(score.HasValue); - ClassicAssert.AreEqual(-1, score.Value); + set = db2.StringSet(db2Key2, db2val); + ClassicAssert.IsTrue(set); + + // Issue DB SAVE for db2 + var res = db1.Execute("SAVE", "1"); + ClassicAssert.AreEqual("OK", res.ToString()); + var expectedLastSave = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + var lastSaveStr = db2.Execute("LASTSAVE").ToString(); + var parsed = long.TryParse(lastSaveStr, out var lastSave); + ClassicAssert.IsTrue(parsed); + ClassicAssert.AreEqual(expectedLastSave, lastSave); + + lastSaveStr = db1.Execute("LASTSAVE").ToString(); + parsed = long.TryParse(lastSaveStr, out lastSave); + ClassicAssert.IsTrue(parsed); + ClassicAssert.AreEqual(0, lastSave); } - server.Store.CommitAOF(true); server.Dispose(false); - server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, tryRecover: true, enableAOF: true); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, tryRecover: true); server.Start(); using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig())) { var db1 = redis.GetDatabase(0); - var score = db1.SortedSetScore(db1Key, "db1:a"); - ClassicAssert.IsTrue(score.HasValue); - ClassicAssert.AreEqual(1, score.Value); + ClassicAssert.IsFalse(db1.KeyExists(db1Key1)); + ClassicAssert.IsFalse(db1.KeyExists(db1Key2)); var db2 = redis.GetDatabase(1); - score = db2.SortedSetScore(db2Key, "db2:a"); + var score = db2.SortedSetScore(db2Key1, "db2:a"); ClassicAssert.IsTrue(score.HasValue); ClassicAssert.AreEqual(-1, score.Value); + + var value = db2.StringGet(db2Key2); + ClassicAssert.IsTrue(value.HasValue); + ClassicAssert.AreEqual(db2val, value.ToString()); } } From 9624d31ee18c06c2207ce4df814856172a3eb39e Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 13 Feb 2025 18:26:54 -0800 Subject: [PATCH 44/82] wip --- libs/server/Resp/AdminCommands.cs | 80 +++++- libs/server/Servers/StoreApi.cs | 2 +- libs/server/StoreWrapper.cs | 19 +- .../GarnetCommandsDocs.json | 54 +++- .../GarnetCommandsInfo.json | 10 + test/Garnet.test/MultiDatabaseTests.cs | 253 ++++++++++++++++-- 6 files changed, 385 insertions(+), 33 deletions(-) diff --git a/libs/server/Resp/AdminCommands.cs b/libs/server/Resp/AdminCommands.cs index 66ff7e3519..5d050fc648 100644 --- a/libs/server/Resp/AdminCommands.cs +++ b/libs/server/Resp/AdminCommands.cs @@ -158,9 +158,9 @@ static void OnACLOrNoScriptFailure(RespServerSession self, RespCommand cmd) } } - void CommitAof() + void CommitAof(int dbId = -1) { - storeWrapper.CommitAOFAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + storeWrapper.CommitAOFAsync(dbId).ConfigureAwait(false).GetAwaiter().GetResult(); } private bool NetworkMonitor() @@ -571,12 +571,41 @@ private bool NetworkModuleLoad(CustomCommandManager customCommandManager) private bool NetworkCOMMITAOF() { - if (parseState.Count != 0) + if (parseState.Count > 1) { return AbortWithWrongNumberOfArguments(nameof(RespCommand.COMMITAOF)); } - CommitAof(); + // By default - commit AOF for all active databases, unless database ID specified + var dbId = -1; + + // Check if ID specified + if (parseState.Count == 1) + { + if (!parseState.TryGetInt(0, out dbId)) + { + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref dcurr, dend)) + SendAndReset(); + return true; + } + + if (dbId != 0 && storeWrapper.serverOptions.EnableCluster) + { + // Cluster mode does not allow DBID + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_SELECT_CLUSTER_MODE, ref dcurr, dend)) + SendAndReset(); + return true; + } + + if (dbId >= storeWrapper.serverOptions.MaxDatabases) + { + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_SELECT_INVALID_INDEX, ref dcurr, dend)) + SendAndReset(); + return true; + } + } + + CommitAof(dbId); while (!RespWriteUtils.TryWriteSimpleString("AOF file committed"u8, ref dcurr, dend)) SendAndReset(); @@ -740,7 +769,10 @@ private bool NetworkSAVE() return AbortWithWrongNumberOfArguments(nameof(RespCommand.SAVE)); } + // By default - save all active databases, unless database ID specified var dbId = -1; + + // Check if ID specified if (parseState.Count == 1) { if (!parseState.TryGetInt(0, out dbId)) @@ -748,7 +780,7 @@ private bool NetworkSAVE() while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref dcurr, dend)) SendAndReset(); return true; - } + } if (dbId != 0 && storeWrapper.serverOptions.EnableCluster) { @@ -781,17 +813,46 @@ private bool NetworkSAVE() } /// - /// LASTSAVE + /// LASTSAVE [DBID] /// /// private bool NetworkLASTSAVE() { - if (parseState.Count != 0) + if (parseState.Count > 1) { return AbortWithWrongNumberOfArguments(nameof(RespCommand.LASTSAVE)); } - storeWrapper.TryGetDatabase(activeDbId, out var db); + // By default - get the last saved timestamp for current active database, unless database ID specified + var dbId = activeDbId; + + // Check if ID specified + if (parseState.Count == 1) + { + if (!parseState.TryGetInt(0, out dbId)) + { + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref dcurr, dend)) + SendAndReset(); + return true; + } + + if (dbId != 0 && storeWrapper.serverOptions.EnableCluster) + { + // Cluster mode does not allow DBID + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_SELECT_CLUSTER_MODE, ref dcurr, dend)) + SendAndReset(); + return true; + } + + if (dbId >= storeWrapper.serverOptions.MaxDatabases) + { + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_SELECT_INVALID_INDEX, ref dcurr, dend)) + SendAndReset(); + return true; + } + } + + storeWrapper.TryGetDatabase(dbId, out var db); var seconds = db.LastSaveTime.ToUnixTimeSeconds(); while (!RespWriteUtils.TryWriteInt64(seconds, ref dcurr, dend)) @@ -811,7 +872,9 @@ private bool NetworkBGSAVE() return AbortWithWrongNumberOfArguments(nameof(RespCommand.BGSAVE)); } + // By default - save all active databases, unless database ID specified var dbId = -1; + var tokenIdx = 0; if (parseState.Count > 0) { @@ -819,6 +882,7 @@ private bool NetworkBGSAVE() .EqualsUpperCaseSpanIgnoringCase(CmdStrings.SCHEDULE)) tokenIdx++; + // Check if ID specified if (parseState.Count - tokenIdx > 0) { if (!parseState.TryGetInt(tokenIdx, out dbId)) diff --git a/libs/server/Servers/StoreApi.cs b/libs/server/Servers/StoreApi.cs index c4d9f54665..d0aa937e5d 100644 --- a/libs/server/Servers/StoreApi.cs +++ b/libs/server/Servers/StoreApi.cs @@ -41,7 +41,7 @@ public StoreApi(StoreWrapper storeWrapper) /// /// Commit AOF /// - public ValueTask CommitAOFAsync(CancellationToken token) => storeWrapper.CommitAOFAsync(token); + public ValueTask CommitAOFAsync(CancellationToken token) => storeWrapper.CommitAOFAsync(token: token); /// /// Flush DB (delete all keys) diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index a5d735f776..5402c35b12 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -5,7 +5,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.IO; using System.Net; using System.Net.Sockets; @@ -216,8 +215,8 @@ public StoreWrapper( this.GarnetObjectSerializer = new GarnetObjectSerializer(this.customCommandManager); this.loggingFrequency = TimeSpan.FromSeconds(serverOptions.LoggingFrequency); - // If more than one database allowed, multi-db mode is turned on - this.allowMultiDb = this.serverOptions.MaxDatabases > 1; + // If cluster mode is off and more than one database allowed multi-db mode is turned on + this.allowMultiDb = !this.serverOptions.EnableCluster && this.serverOptions.MaxDatabases > 1; // Create default databases map of size 1 databases = new ExpandableMap(1, 0, this.serverOptions.MaxDatabases - 1); @@ -1024,18 +1023,22 @@ internal async ValueTask WaitForCommitAsync(CancellationToken token = default) } /// - /// Asynchronously wait for AOF commits on all active databases + /// Asynchronously wait for AOF commits on all active databases, + /// unless specific database ID specified (by default: -1 = all) /// + /// Specific database ID to commit AOF for (optional) /// Cancellation token /// ValueTask - internal async ValueTask CommitAOFAsync(CancellationToken token = default) + internal async ValueTask CommitAOFAsync(int dbId = -1, CancellationToken token = default) { if (!serverOptions.EnableAOF || appendOnlyFile == null) return; var activeDbIdsSize = this.activeDbIdsLength; - if (!allowMultiDb || activeDbIdsSize == 1) + if (!allowMultiDb || activeDbIdsSize == 1 || dbId != -1) { - await appendOnlyFile.CommitAsync(token: token); + var dbFound = TryGetDatabase(dbId, out var db); + Debug.Assert(dbFound); + await db.AppendOnlyFile.CommitAsync(token: token); return; } @@ -1045,7 +1048,7 @@ internal async ValueTask CommitAOFAsync(CancellationToken token = default) var tasks = new Task[activeDbIdsSize]; for (var i = 0; i < activeDbIdsSize; i++) { - var dbId = activeDbIdsSnapshot[i]; + dbId = activeDbIdsSnapshot[i]; var db = databasesMapSnapshot[dbId]; tasks[i] = db.AppendOnlyFile.CommitAsync(token: token).AsTask(); } diff --git a/playground/CommandInfoUpdater/GarnetCommandsDocs.json b/playground/CommandInfoUpdater/GarnetCommandsDocs.json index cd840b252e..ac37590efe 100644 --- a/playground/CommandInfoUpdater/GarnetCommandsDocs.json +++ b/playground/CommandInfoUpdater/GarnetCommandsDocs.json @@ -1,4 +1,29 @@ [ + { + "Command": "BGSAVE", + "Name": "BGSAVE", + "Summary": "Asynchronously saves the database(s) to disk.", + "Group": "Server", + "Complexity": "O(1)", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "SCHEDULE", + "DisplayText": "schedule", + "Type": "PureToken", + "Token": "SCHEDULE", + "ArgumentFlags": "Optional" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "DBID", + "DisplayText": "dbid", + "Type": "Integer", + "Token": "DBID", + "ArgumentFlags": "Optional" + } + ] + }, { "Command": "CLIENT_KILL", "Name": "CLIENT|KILL", @@ -140,7 +165,17 @@ "Command": "COMMITAOF", "Name": "COMMITAOF", "Group": "Server", - "Summary": "Commit to append-only file." + "Summary": "Commit to append-only file.", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "DBID", + "DisplayText": "dbid", + "Type": "Integer", + "Token": "DBID", + "ArgumentFlags": "Optional" + } + ] }, { "Command": "COSCAN", @@ -513,6 +548,23 @@ } ] }, + { + "Command": "LASTSAVE", + "Name": "LASTSAVE", + "Summary": "Returns the Unix timestamp of the last successful save to disk.", + "Group": "Server", + "Complexity": "O(1)", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "DBID", + "DisplayText": "dbid", + "Type": "Integer", + "Token": "DBID", + "ArgumentFlags": "Optional" + } + ] + }, { "Command": "PURGEBP", "Name": "PURGEBP", diff --git a/playground/CommandInfoUpdater/GarnetCommandsInfo.json b/playground/CommandInfoUpdater/GarnetCommandsInfo.json index 601c556cd3..fdbeff5722 100644 --- a/playground/CommandInfoUpdater/GarnetCommandsInfo.json +++ b/playground/CommandInfoUpdater/GarnetCommandsInfo.json @@ -615,6 +615,16 @@ } ] }, + { + "Command": "LASTSAVE", + "Name": "LASTSAVE", + "Arity": -1, + "Flags": "Fast, Loading, Stale", + "AclCategories": "Admin, Dangerous, Fast", + "Tips": [ + "nondeterministic_output" + ] + }, { "Command": "PURGEBP", "Name": "PURGEBP", diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index c3cef4b15b..1a324bf0d6 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Garnet.common; +using Garnet.server; using Newtonsoft.Json.Linq; using NUnit.Framework; using NUnit.Framework.Legacy; @@ -20,7 +21,7 @@ public class MultiDatabaseTests public void Setup() { TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); - server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, enableAOF: true, lowMemory: true); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, enableAOF: true, lowMemory: true, commitFrequencyMs: 1000); server.Start(); } @@ -575,19 +576,27 @@ public void MultiDatabaseAofRecoverObjectTest() } [Test] - public void MultiDatabaseSaveByIdTest() + [TestCase(false)] + [TestCase(true)] + public void MultiDatabaseSaveRecoverByDbIdTest(bool backgroundSave) { var db1Key1 = "db1:key1"; var db1Key2 = "db1:key2"; var db2Key1 = "db2:key1"; var db2Key2 = "db2:key2"; + var db2Key3 = "db2:key3"; + var db2Key4 = "db2:key4"; var db1val = new RedisValue("db1:a"); - var db2val = new RedisValue("db2:a"); + var db2val1 = new RedisValue("db2:a"); + var db2val2 = new RedisValue("db2:b"); var db1data = new SortedSetEntry[] { new("db1:a", 1), new("db1:b", 2), new("db1:c", 3) }; - var db2data = new SortedSetEntry[] { new("db2:a", -1), new("db2:b", -2), new("db2:c", -3) }; + var db2data1 = new SortedSetEntry[] { new("db2:a", -1), new("db2:b", -2), new("db2:c", -3) }; + var db2data2 = new SortedSetEntry[] { new("db2:d", 4), new("db2:e", 5), new("db2:f", 6) }; + long expectedLastSave; using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig())) { + // Add object & raw string data to DB 0 var db1 = redis.GetDatabase(0); var added = db1.SortedSetAdd(db1Key1, db1data); ClassicAssert.AreEqual(3, added); @@ -595,39 +604,201 @@ public void MultiDatabaseSaveByIdTest() var set = db1.StringSet(db1Key2, db1val); ClassicAssert.IsTrue(set); + // Add object & raw string data to DB 1 var db2 = redis.GetDatabase(1); - added = db2.SortedSetAdd(db2Key1, db2data); + added = db2.SortedSetAdd(db2Key1, db2data1); ClassicAssert.AreEqual(3, added); - set = db2.StringSet(db2Key2, db2val); + set = db2.StringSet(db2Key2, db2val1); ClassicAssert.IsTrue(set); - // Issue DB SAVE for db2 - var res = db1.Execute("SAVE", "1"); - ClassicAssert.AreEqual("OK", res.ToString()); - var expectedLastSave = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - var lastSaveStr = db2.Execute("LASTSAVE").ToString(); - var parsed = long.TryParse(lastSaveStr, out var lastSave); - ClassicAssert.IsTrue(parsed); - ClassicAssert.AreEqual(expectedLastSave, lastSave); + // Issue DB SAVE for DB 1 + var res = db1.Execute(backgroundSave ? "BGSAVE" : "SAVE", "1"); + ClassicAssert.AreEqual(backgroundSave ? "Background saving started" : "OK", res.ToString()); + + var lastSave = 0L; + string lastSaveStr; + bool parsed; + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + while (!cts.IsCancellationRequested) + { + // Verify DB 1 was saved by checking LASTSAVE + lastSaveStr = db1.Execute("LASTSAVE", "1").ToString(); + parsed = long.TryParse(lastSaveStr, out lastSave); + ClassicAssert.IsTrue(parsed); + if (lastSave != 0) + break; + Task.Delay(TimeSpan.FromMilliseconds(100), cts.Token); + } + expectedLastSave = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + Assert.That(lastSave, Is.InRange(expectedLastSave - 1, expectedLastSave)); + + // Verify DB 0 was not saved lastSaveStr = db1.Execute("LASTSAVE").ToString(); parsed = long.TryParse(lastSaveStr, out lastSave); ClassicAssert.IsTrue(parsed); ClassicAssert.AreEqual(0, lastSave); } + // Restart server + server.Dispose(false); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, tryRecover: true); + server.Start(); + + using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig())) + { + // Verify that data was not recovered for DB 0 + var db1 = redis.GetDatabase(0); + + ClassicAssert.IsFalse(db1.KeyExists(db1Key1)); + ClassicAssert.IsFalse(db1.KeyExists(db1Key2)); + + // Verify that data was recovered for DB 1 + var db2 = redis.GetDatabase(1); + + var score = db2.SortedSetScore(db2Key1, "db2:a"); + ClassicAssert.IsTrue(score.HasValue); + ClassicAssert.AreEqual(-1, score.Value); + + var value = db2.StringGet(db2Key2); + ClassicAssert.IsTrue(value.HasValue); + ClassicAssert.AreEqual(db2val1, value.ToString()); + + // Re-add object & raw string data to DB 0 + var added = db1.SortedSetAdd(db1Key1, db1data); + ClassicAssert.AreEqual(3, added); + + var set = db1.StringSet(db1Key2, db1val); + ClassicAssert.IsTrue(set); + + // Add new object & raw string data to DB 1 + added = db2.SortedSetAdd(db2Key3, db2data2); + ClassicAssert.AreEqual(3, added); + + set = db2.StringSet(db2Key4, db2val2); + ClassicAssert.IsTrue(set); + + // Issue DB SAVE for DB 0 + var res = db1.Execute(backgroundSave ? "BGSAVE" : "SAVE", "0"); + ClassicAssert.AreEqual(backgroundSave ? "Background saving started" : "OK", res.ToString()); + + // Verify DB 0 was saved by checking LASTSAVE + var lastSave = 0L; + string lastSaveStr; + bool parsed; + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + while (!cts.IsCancellationRequested) + { + // Verify DB 1 was saved by checking LASTSAVE + lastSaveStr = db1.Execute("LASTSAVE").ToString(); + parsed = long.TryParse(lastSaveStr, out lastSave); + ClassicAssert.IsTrue(parsed); + if (lastSave != 0) + break; + Task.Delay(TimeSpan.FromMilliseconds(100), cts.Token); + } + + var prevLastSave = expectedLastSave; + expectedLastSave = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + Assert.That(lastSave, Is.InRange(expectedLastSave - 1, expectedLastSave)); + + // Verify DB 1 was not saved + Thread.Sleep(TimeSpan.FromSeconds(1)); + lastSaveStr = db1.Execute("LASTSAVE", "1").ToString(); + parsed = long.TryParse(lastSaveStr, out lastSave); + ClassicAssert.IsTrue(parsed); + ClassicAssert.AreEqual(prevLastSave, lastSave); + } + + // Restart server server.Dispose(false); server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, tryRecover: true); server.Start(); using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig())) { + // Verify that data was recovered for DB 0 + var db1 = redis.GetDatabase(0); + + var score = db1.SortedSetScore(db1Key1, "db1:a"); + ClassicAssert.IsTrue(score.HasValue); + ClassicAssert.AreEqual(1, score.Value); + + var value = db1.StringGet(db1Key2); + ClassicAssert.IsTrue(value.HasValue); + ClassicAssert.AreEqual(db1val, value.ToString()); + + // Verify that previous data was recovered for DB 1 + var db2 = redis.GetDatabase(1); + + score = db2.SortedSetScore(db2Key1, "db2:a"); + ClassicAssert.IsTrue(score.HasValue); + ClassicAssert.AreEqual(-1, score.Value); + + value = db2.StringGet(db2Key2); + ClassicAssert.IsTrue(value.HasValue); + ClassicAssert.AreEqual(db2val1, value.ToString()); + + // Verify that new data was not recovered for DB 1 + ClassicAssert.IsFalse(db1.KeyExists(db2Key3)); + ClassicAssert.IsFalse(db1.KeyExists(db2Key4)); + } + } + + [Test] + public void MultiDatabaseAofRecoverByDbIdTest() + { + var db1Key1 = "db1:key1"; + var db1Key2 = "db1:key2"; + var db2Key1 = "db2:key1"; + var db2Key2 = "db2:key2"; + var db2Key3 = "db2:key3"; + var db2Key4 = "db2:key4"; + var db1val = new RedisValue("db1:a"); + var db2val1 = new RedisValue("db2:a"); + var db2val2 = new RedisValue("db2:b"); + var db1data = new SortedSetEntry[] { new("db1:a", 1), new("db1:b", 2), new("db1:c", 3) }; + var db2data1 = new SortedSetEntry[] { new("db2:a", -1), new("db2:b", -2), new("db2:c", -3) }; + var db2data2 = new SortedSetEntry[] { new("db2:d", 4), new("db2:e", 5), new("db2:f", 6) }; + + using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig())) + { + // Add object & raw string data to DB 0 + var db1 = redis.GetDatabase(0); + var added = db1.SortedSetAdd(db1Key1, db1data); + ClassicAssert.AreEqual(3, added); + + var set = db1.StringSet(db1Key2, db1val); + ClassicAssert.IsTrue(set); + + // Add object & raw string data to DB 1 + var db2 = redis.GetDatabase(1); + added = db2.SortedSetAdd(db2Key1, db2data1); + ClassicAssert.AreEqual(3, added); + + set = db2.StringSet(db2Key2, db2val1); + ClassicAssert.IsTrue(set); + + // Issue COMMITAOF for DB 1 + var res = db1.Execute("COMMITAOF", "1"); + ClassicAssert.AreEqual("AOF file committed", res.ToString()); + } + + // Restart server + server.Dispose(false); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, tryRecover: true, enableAOF: true); + server.Start(); + + using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig())) + { + // Verify that data was not recovered for DB 0 var db1 = redis.GetDatabase(0); ClassicAssert.IsFalse(db1.KeyExists(db1Key1)); ClassicAssert.IsFalse(db1.KeyExists(db1Key2)); + // Verify that data was recovered for DB 1 var db2 = redis.GetDatabase(1); var score = db2.SortedSetScore(db2Key1, "db2:a"); @@ -636,7 +807,59 @@ public void MultiDatabaseSaveByIdTest() var value = db2.StringGet(db2Key2); ClassicAssert.IsTrue(value.HasValue); - ClassicAssert.AreEqual(db2val, value.ToString()); + ClassicAssert.AreEqual(db2val1, value.ToString()); + + // Re-add object & raw string data to DB 0 + var added = db1.SortedSetAdd(db1Key1, db1data); + ClassicAssert.AreEqual(3, added); + + var set = db1.StringSet(db1Key2, db1val); + ClassicAssert.IsTrue(set); + + // Add new object & raw string data to DB 1 + added = db2.SortedSetAdd(db2Key3, db2data2); + ClassicAssert.AreEqual(3, added); + + set = db2.StringSet(db2Key4, db2val2); + ClassicAssert.IsTrue(set); + + // Issue COMMITAOF for DB 0 + var res = db1.Execute("COMMITAOF", "0"); + ClassicAssert.AreEqual("AOF file committed", res.ToString()); + } + + // Restart server + server.Dispose(false); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, tryRecover: true, enableAOF: true); + server.Start(); + + using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig())) + { + // Verify that data was recovered for DB 0 + var db1 = redis.GetDatabase(0); + + var score = db1.SortedSetScore(db1Key1, "db1:a"); + ClassicAssert.IsTrue(score.HasValue); + ClassicAssert.AreEqual(1, score.Value); + + var value = db1.StringGet(db1Key2); + ClassicAssert.IsTrue(value.HasValue); + ClassicAssert.AreEqual(db1val, value.ToString()); + + // Verify that previous data was recovered for DB 1 + var db2 = redis.GetDatabase(1); + + score = db2.SortedSetScore(db2Key1, "db2:a"); + ClassicAssert.IsTrue(score.HasValue); + ClassicAssert.AreEqual(-1, score.Value); + + value = db2.StringGet(db2Key2); + ClassicAssert.IsTrue(value.HasValue); + ClassicAssert.AreEqual(db2val1, value.ToString()); + + // Verify that new data was not recovered for DB 1 + ClassicAssert.IsFalse(db1.KeyExists(db2Key3)); + ClassicAssert.IsFalse(db1.KeyExists(db2Key4)); } } From ae6026c7343b331a40f89523a14373d689d1c3d8 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 13 Feb 2025 19:05:22 -0800 Subject: [PATCH 45/82] Adding HCOLLECT info & docs to GarnetCommandDocs/Info & updating RespCommandDocs/Info --- libs/resources/RespCommandsDocs.json | 179 ++++++++++------ libs/resources/RespCommandsInfo.json | 197 +++++++++--------- .../GarnetCommandsDocs.json | 6 + .../GarnetCommandsInfo.json | 25 +++ 4 files changed, 252 insertions(+), 155 deletions(-) diff --git a/libs/resources/RespCommandsDocs.json b/libs/resources/RespCommandsDocs.json index 15cc917695..9e217fb06f 100644 --- a/libs/resources/RespCommandsDocs.json +++ b/libs/resources/RespCommandsDocs.json @@ -1027,6 +1027,14 @@ "Token": "NO" } ] + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "MAXAGE", + "DisplayText": "maxage", + "Type": "Integer", + "Token": "MAXAGE", + "ArgumentFlags": "Optional" } ] } @@ -1175,7 +1183,7 @@ { "Command": "CLUSTER", "Name": "CLUSTER", - "Summary": "A container for Redis Cluster commands.", + "Summary": "A container for Redis Cluster internal commands.", "Group": "Cluster", "Complexity": "Depends on subcommand.", "SubCommands": [ @@ -1429,6 +1437,13 @@ "Group": "Cluster", "Complexity": "O(N) where N is the total number of Cluster nodes" }, + { + "Command": "CLUSTER_PUBLISH", + "Name": "CLUSTER|PUBLISH", + "Summary": "Processes a forwarded published message from any node in the cluster", + "Group": "Cluster", + "Complexity": "O(1)" + }, { "Command": "CLUSTER_REPLICAS", "Name": "CLUSTER|REPLICAS", @@ -1570,6 +1585,13 @@ "Complexity": "O(N) where N is the total number of Cluster nodes", "DocFlags": "Deprecated", "ReplacedBy": "\u0060CLUSTER SHARDS\u0060" + }, + { + "Command": "CLUSTER_SPUBLISH", + "Name": "CLUSTER|SPUBLISH", + "Summary": "Processes a forwarded published message from a node in the same shard", + "Group": "Cluster", + "Complexity": "O(1)" } ] }, @@ -1603,22 +1625,6 @@ } ] }, - { - "Command": "COMMAND_INFO", - "Name": "COMMAND|INFO", - "Summary": "Returns information about one, multiple or all commands.", - "Group": "Server", - "Complexity": "O(N) where N is the number of commands to look up", - "Arguments": [ - { - "TypeDiscriminator": "RespCommandBasicArgument", - "Name": "COMMAND-NAME", - "DisplayText": "command-name", - "Type": "String", - "ArgumentFlags": "Optional, Multiple" - } - ] - }, { "Command": "COMMAND_GETKEYS", "Name": "COMMAND|GETKEYS", @@ -1662,6 +1668,22 @@ "ArgumentFlags": "Optional, Multiple" } ] + }, + { + "Command": "COMMAND_INFO", + "Name": "COMMAND|INFO", + "Summary": "Returns information about one, multiple or all commands.", + "Group": "Server", + "Complexity": "O(N) where N is the number of commands to look up", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "COMMAND-NAME", + "DisplayText": "command-name", + "Type": "String", + "ArgumentFlags": "Optional, Multiple" + } + ] } ] }, @@ -1780,9 +1802,10 @@ { "Command": "DEBUG", "Name": "DEBUG", - "Summary": "Depends on subcommand.", + "Summary": "A container for debugging commands.", "Group": "Server", - "Complexity": "O(1)" + "Complexity": "Depends on subcommand.", + "DocFlags": "SysCmd" }, { "Command": "DECR", @@ -3890,6 +3913,14 @@ "Type": "Integer", "Token": "COUNT", "ArgumentFlags": "Optional" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "NOVALUES", + "DisplayText": "novalues", + "Type": "PureToken", + "Token": "NOVALUES", + "ArgumentFlags": "Optional" } ] }, @@ -5419,7 +5450,7 @@ "Name": "NUMPARAMS", "DisplayText": "numParams", "Type": "Integer", - "Summary": "Number of parameters of the command to register" + "Summary": "Numer of parameters of the command to register" }, { "TypeDiscriminator": "RespCommandBasicArgument", @@ -5615,12 +5646,51 @@ }, { "TypeDiscriminator": "RespCommandBasicArgument", - "Name": "SERIALIZEDVALUE", + "Name": "SERIALIZED-VALUE", "DisplayText": "serialized-value", "Type": "String" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "REPLACE", + "DisplayText": "replace", + "Type": "PureToken", + "Token": "REPLACE", + "ArgumentFlags": "Optional" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "ABSTTL", + "DisplayText": "absttl", + "Type": "PureToken", + "Token": "ABSTTL", + "ArgumentFlags": "Optional" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "SECONDS", + "DisplayText": "seconds", + "Type": "Integer", + "Token": "IDLETIME", + "ArgumentFlags": "Optional" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "FREQUENCY", + "DisplayText": "frequency", + "Type": "Integer", + "Token": "FREQ", + "ArgumentFlags": "Optional" } ] }, + { + "Command": "ROLE", + "Name": "ROLE", + "Summary": "Returns the replication role.", + "Group": "Server", + "Complexity": "O(1)" + }, { "Command": "RPOP", "Name": "RPOP", @@ -6558,6 +6628,27 @@ } ] }, + { + "Command": "SPUBLISH", + "Name": "SPUBLISH", + "Summary": "Post a message to a shard channel", + "Group": "PubSub", + "Complexity": "O(N) where N is the number of clients subscribed to the receiving shard channel.", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "SHARDCHANNEL", + "DisplayText": "shardchannel", + "Type": "String" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "MESSAGE", + "DisplayText": "message", + "Type": "String" + } + ] + }, { "Command": "SRANDMEMBER", "Name": "SRANDMEMBER", @@ -6643,23 +6734,18 @@ ] }, { - "Command": "SPUBLISH", - "Name": "SPUBLISH", - "Summary": "Posts a message to a shard channel.", + "Command": "SSUBSCRIBE", + "Name": "SSUBSCRIBE", + "Summary": "Listens for messages published to shard channels.", "Group": "PubSub", - "Complexity": "O(N) where N is the number of clients subscribed to the receiving shard channel.", + "Complexity": "O(N) where N is the number of shard channels to subscribe to.", "Arguments": [ { "TypeDiscriminator": "RespCommandBasicArgument", "Name": "SHARDCHANNEL", "DisplayText": "shardchannel", - "Type": "String" - }, - { - "TypeDiscriminator": "RespCommandBasicArgument", - "Name": "MESSAGE", - "DisplayText": "message", - "Type": "String" + "Type": "String", + "ArgumentFlags": "Multiple" } ] }, @@ -6695,22 +6781,6 @@ } ] }, - { - "Command": "SSUBSCRIBE", - "Name": "SSUBSCRIBE", - "Summary": "Listens for messages published to shard channels.", - "Group": "PubSub", - "Complexity": "O(N) where N is the number of shard channels to subscribe to.", - "Arguments": [ - { - "TypeDiscriminator": "RespCommandBasicArgument", - "Name": "shardchannel", - "DisplayText": "channel", - "Type": "String", - "ArgumentFlags": "Multiple" - } - ] - }, { "Command": "SUBSTR", "Name": "SUBSTR", @@ -6821,13 +6891,6 @@ } ] }, - { - "Command": "ROLE", - "Name": "ROLE", - "Summary": "Returns the replication role.", - "Group": "Server", - "Complexity": "O(1)" - }, { "Command": "UNLINK", "Name": "UNLINK", @@ -7593,7 +7656,7 @@ { "Command": "ZRANGEBYLEX", "Name": "ZRANGEBYLEX", - "Summary": "Returns the number of members in a sorted set within a lexicographical range.", + "Summary": "Returns members in a sorted set within a lexicographical range.", "Group": "SortedSet", "Complexity": "O(log(N)\u002BM) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)).", "DocFlags": "Deprecated", @@ -7610,13 +7673,13 @@ "TypeDiscriminator": "RespCommandBasicArgument", "Name": "MIN", "DisplayText": "min", - "Type": "Double" + "Type": "String" }, { "TypeDiscriminator": "RespCommandBasicArgument", "Name": "MAX", "DisplayText": "max", - "Type": "Double" + "Type": "String" }, { "TypeDiscriminator": "RespCommandContainerArgument", diff --git a/libs/resources/RespCommandsInfo.json b/libs/resources/RespCommandsInfo.json index 2d84a2c6ce..e3f5de4b47 100644 --- a/libs/resources/RespCommandsInfo.json +++ b/libs/resources/RespCommandsInfo.json @@ -617,43 +617,6 @@ "Flags": "Admin, NoMulti, NoScript", "AclCategories": "Admin, Dangerous, Slow, Garnet" }, - { - "Command": "CLUSTER_PUBLISH", - "Name": "CLUSTER|PUBLISH", - "IsInternal": true, - "Arity": 4, - "Flags": "Loading, NoScript, PubSub, Stale", - "FirstKey": 1, - "LastKey": 1, - "Step": 1, - "AclCategories": "Admin, PubSub, Slow, Garnet" - }, - { - "Command": "CLUSTER_SPUBLISH", - "Name": "CLUSTER|SPUBLISH", - "IsInternal": true, - "Arity": 4, - "Flags": "Loading, NoScript, PubSub, Stale", - "FirstKey": 1, - "LastKey": 1, - "Step": 1, - "AclCategories": "Admin, PubSub, Slow, Garnet", - "KeySpecifications": [ - { - "BeginSearch": { - "TypeDiscriminator": "BeginSearchIndex", - "Index": 1 - }, - "FindKeys": { - "TypeDiscriminator": "FindKeysRange", - "LastKey": 0, - "KeyStep": 1, - "Limit": 0 - }, - "Flags": "RO" - } - ] - }, { "Command": "CLUSTER_BUMPEPOCH", "Name": "CLUSTER|BUMPEPOCH", @@ -837,6 +800,17 @@ "nondeterministic_output" ] }, + { + "Command": "CLUSTER_PUBLISH", + "Name": "CLUSTER|PUBLISH", + "IsInternal": true, + "Arity": 4, + "Flags": "Loading, NoScript, PubSub, Stale", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Admin, PubSub, Slow, Garnet" + }, { "Command": "CLUSTER_REPLICAS", "Name": "CLUSTER|REPLICAS", @@ -926,6 +900,32 @@ "Arity": 1, "Flags": "Admin, NoMulti, NoScript", "AclCategories": "Admin, Dangerous, Slow, Garnet" + }, + { + "Command": "CLUSTER_SPUBLISH", + "Name": "CLUSTER|SPUBLISH", + "IsInternal": true, + "Arity": 4, + "Flags": "Loading, NoScript, PubSub, Stale", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Admin, PubSub, Slow, Garnet", + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Flags": "RO" + } + ] } ] }, @@ -956,16 +956,6 @@ "nondeterministic_output_order" ] }, - { - "Command": "COMMAND_INFO", - "Name": "COMMAND|INFO", - "Arity": -2, - "Flags": "Loading, Stale", - "AclCategories": "Connection, Slow", - "Tips": [ - "nondeterministic_output_order" - ] - }, { "Command": "COMMAND_GETKEYS", "Name": "COMMAND|GETKEYS", @@ -979,6 +969,16 @@ "Arity": -3, "Flags": "Loading, Stale", "AclCategories": "Connection, Slow" + }, + { + "Command": "COMMAND_INFO", + "Name": "COMMAND|INFO", + "Arity": -2, + "Flags": "Loading, Stale", + "AclCategories": "Connection, Slow", + "Tips": [ + "nondeterministic_output_order" + ] } ] }, @@ -1082,7 +1082,7 @@ "Command": "DEBUG", "Name": "DEBUG", "Arity": -2, - "Flags": "Admin, Noscript, Loading, Stale", + "Flags": "Admin, Loading, NoScript, Stale", "AclCategories": "Admin, Dangerous, Slow" }, { @@ -1179,7 +1179,10 @@ "FirstKey": 1, "LastKey": 1, "Step": 1, - "AclCategories": "KeySpace, Read", + "AclCategories": "KeySpace, Read, Slow", + "Tips": [ + "nondeterministic_output" + ], "KeySpecifications": [ { "BeginSearch": { @@ -3569,13 +3572,20 @@ "FindKeys": { "TypeDiscriminator": "FindKeysRange", "LastKey": 0, - "KeyStep": 0, + "KeyStep": 1, "Limit": 0 }, "Flags": "OW, Update" } ] }, + { + "Command": "ROLE", + "Name": "ROLE", + "Arity": 1, + "Flags": "Fast, Loading, NoScript, Stale", + "AclCategories": "Admin, Dangerous, Fast" + }, { "Command": "RPOP", "Name": "RPOP", @@ -4110,7 +4120,7 @@ "KeyStep": 1, "Limit": 0 }, - "Flags": "RW, Update" + "Flags": "OW, Update" }, { "BeginSearch": { @@ -4327,6 +4337,31 @@ } ] }, + { + "Command": "SPUBLISH", + "Name": "SPUBLISH", + "Arity": 3, + "Flags": "Fast, Loading, PubSub, Stale", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, PubSub", + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Flags": "NotKey" + } + ] + }, { "Command": "SRANDMEMBER", "Name": "SRANDMEMBER", @@ -4408,38 +4443,6 @@ } ] }, - { - "Command": "STRLEN", - "Name": "STRLEN", - "Arity": 2, - "Flags": "Fast, ReadOnly", - "FirstKey": 1, - "LastKey": 1, - "Step": 1, - "AclCategories": "Fast, Read, String", - "KeySpecifications": [ - { - "BeginSearch": { - "TypeDiscriminator": "BeginSearchIndex", - "Index": 1 - }, - "FindKeys": { - "TypeDiscriminator": "FindKeysRange", - "LastKey": 0, - "KeyStep": 1, - "Limit": 0 - }, - "Flags": "RO" - } - ] - }, - { - "Command": "SUBSCRIBE", - "Name": "SUBSCRIBE", - "Arity": -2, - "Flags": "Loading, NoScript, PubSub, Stale", - "AclCategories": "PubSub, Slow" - }, { "Command": "SSUBSCRIBE", "Name": "SSUBSCRIBE", @@ -4448,7 +4451,7 @@ "FirstKey": 1, "LastKey": -1, "Step": 1, - "AclCategories": "PubSub, Slow, Read", + "AclCategories": "PubSub, Slow", "KeySpecifications": [ { "BeginSearch": { @@ -4461,19 +4464,19 @@ "KeyStep": 1, "Limit": 0 }, - "Flags": "RO" + "Flags": "NotKey" } ] }, { - "Command": "SPUBLISH", - "Name": "SPUBLISH", - "Arity": 3, - "Flags": "Loading, NoScript, PubSub, Stale", + "Command": "STRLEN", + "Name": "STRLEN", + "Arity": 2, + "Flags": "Fast, ReadOnly", "FirstKey": 1, "LastKey": 1, "Step": 1, - "AclCategories": "PubSub, Slow, Read", + "AclCategories": "Fast, Read, String", "KeySpecifications": [ { "BeginSearch": { @@ -4490,6 +4493,13 @@ } ] }, + { + "Command": "SUBSCRIBE", + "Name": "SUBSCRIBE", + "Arity": -2, + "Flags": "Loading, NoScript, PubSub, Stale", + "AclCategories": "PubSub, Slow" + }, { "Command": "SUBSTR", "Name": "SUBSTR", @@ -4644,13 +4654,6 @@ } ] }, - { - "Command": "ROLE", - "Name": "ROLE", - "Arity": 1, - "Flags": "NoScript, Loading, Stale, Fast", - "AclCategories": "Admin, Fast, Dangerous" - }, { "Command": "UNLINK", "Name": "UNLINK", diff --git a/playground/CommandInfoUpdater/GarnetCommandsDocs.json b/playground/CommandInfoUpdater/GarnetCommandsDocs.json index 84c80cea0e..bf0bd62dc9 100644 --- a/playground/CommandInfoUpdater/GarnetCommandsDocs.json +++ b/playground/CommandInfoUpdater/GarnetCommandsDocs.json @@ -186,6 +186,12 @@ "Summary": "Forces garbage collection.", "Group": "Server" }, + { + "Command": "HCOLLECT", + "Name": "HCOLLECT", + "Summary": "Manually trigger deletion of expired fields from memory", + "Group": "Hash" + }, { "Command": "SECONDARYOF", "Name": "SECONDARYOF", diff --git a/playground/CommandInfoUpdater/GarnetCommandsInfo.json b/playground/CommandInfoUpdater/GarnetCommandsInfo.json index 04570affc7..ba349c0f18 100644 --- a/playground/CommandInfoUpdater/GarnetCommandsInfo.json +++ b/playground/CommandInfoUpdater/GarnetCommandsInfo.json @@ -393,6 +393,31 @@ "KeySpecifications": null, "SubCommands": null }, + { + "Command": "HCOLLECT", + "Name": "HCOLLECT", + "Arity": 2, + "Flags": "Admin, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Admin, Hash, Write, Garnet", + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Flags": "RW, Access, Update" + } + ] + }, { "Command": "LATENCY", "Name": "LATENCY", From b8e75e11763546e1d29b0e856219e1aaa36bb222 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 13 Feb 2025 19:16:18 -0800 Subject: [PATCH 46/82] Updated command info & docs for SAVE, BGSAVE, LASTSAVE & COMMITAOF --- libs/resources/RespCommandsDocs.json | 44 ++++++++++++++++++++++++++-- libs/resources/RespCommandsInfo.json | 2 +- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/libs/resources/RespCommandsDocs.json b/libs/resources/RespCommandsDocs.json index 9e217fb06f..4f482776fe 100644 --- a/libs/resources/RespCommandsDocs.json +++ b/libs/resources/RespCommandsDocs.json @@ -198,6 +198,14 @@ "Type": "PureToken", "Token": "SCHEDULE", "ArgumentFlags": "Optional" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "DBID", + "DisplayText": "dbid", + "Type": "Integer", + "Token": "DBID", + "ArgumentFlags": "Optional" } ] }, @@ -1691,7 +1699,17 @@ "Command": "COMMITAOF", "Name": "COMMITAOF", "Summary": "Commit to append-only file.", - "Group": "Server" + "Group": "Server", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "DBID", + "DisplayText": "dbid", + "Type": "Integer", + "Token": "DBID", + "ArgumentFlags": "Optional" + } + ] }, { "Command": "CONFIG", @@ -4159,7 +4177,17 @@ "Name": "LASTSAVE", "Summary": "Returns the Unix timestamp of the last successful save to disk.", "Group": "Server", - "Complexity": "O(1)" + "Complexity": "O(1)", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "DBID", + "DisplayText": "dbid", + "Type": "Integer", + "Token": "DBID", + "ArgumentFlags": "Optional" + } + ] }, { "Command": "LATENCY", @@ -5836,7 +5864,17 @@ "Name": "SAVE", "Summary": "Synchronously saves the database(s) to disk.", "Group": "Server", - "Complexity": "O(N) where N is the total number of keys in all databases" + "Complexity": "O(N) where N is the total number of keys in all databases", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "DBID", + "DisplayText": "dbid", + "Type": "Integer", + "Token": "DBID", + "ArgumentFlags": "Optional" + } + ] }, { "Command": "SCAN", diff --git a/libs/resources/RespCommandsInfo.json b/libs/resources/RespCommandsInfo.json index 8855c7fafe..7ef6d93e55 100644 --- a/libs/resources/RespCommandsInfo.json +++ b/libs/resources/RespCommandsInfo.json @@ -2536,7 +2536,7 @@ { "Command": "LASTSAVE", "Name": "LASTSAVE", - "Arity": 1, + "Arity": -1, "Flags": "Fast, Loading, Stale", "AclCategories": "Admin, Dangerous, Fast", "Tips": [ From 94778d48005c34f67d79eab6b003238f1e4f11e1 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 13 Feb 2025 19:21:00 -0800 Subject: [PATCH 47/82] format --- libs/server/Resp/AdminCommands.cs | 2 +- libs/server/StoreWrapper.cs | 4 ++-- test/Garnet.test/MultiDatabaseTests.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/server/Resp/AdminCommands.cs b/libs/server/Resp/AdminCommands.cs index f0794e9523..88dcc3545d 100644 --- a/libs/server/Resp/AdminCommands.cs +++ b/libs/server/Resp/AdminCommands.cs @@ -860,7 +860,7 @@ private bool NetworkSAVE() while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref dcurr, dend)) SendAndReset(); return true; - } + } if (dbId != 0 && storeWrapper.serverOptions.EnableCluster) { diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 5402c35b12..60b6934aa7 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -1657,10 +1657,10 @@ private bool TryGetSavedDatabaseIds(string path, string baseName, out int[] dbId var dirName = new DirectoryInfo(dirs[i]).Name; var sepIdx = dirName.IndexOf('_'); var dbId = 0; - + if (sepIdx != -1 && !int.TryParse(dirName.AsSpan(sepIdx + 1), out dbId)) continue; - + dbIds[i] = dbId; } diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index 1a324bf0d6..06d328f5d2 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -615,7 +615,7 @@ public void MultiDatabaseSaveRecoverByDbIdTest(bool backgroundSave) // Issue DB SAVE for DB 1 var res = db1.Execute(backgroundSave ? "BGSAVE" : "SAVE", "1"); ClassicAssert.AreEqual(backgroundSave ? "Background saving started" : "OK", res.ToString()); - + var lastSave = 0L; string lastSaveStr; bool parsed; @@ -682,7 +682,7 @@ public void MultiDatabaseSaveRecoverByDbIdTest(bool backgroundSave) // Issue DB SAVE for DB 0 var res = db1.Execute(backgroundSave ? "BGSAVE" : "SAVE", "0"); ClassicAssert.AreEqual(backgroundSave ? "Background saving started" : "OK", res.ToString()); - + // Verify DB 0 was saved by checking LASTSAVE var lastSave = 0L; string lastSaveStr; From 8cafce3bbb3e3647a38aeff117e3a04081a1bb1a Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 13 Feb 2025 20:27:22 -0800 Subject: [PATCH 48/82] bugfixes --- libs/server/StoreWrapper.cs | 6 ++++-- test/Garnet.test/MultiDatabaseTests.cs | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 60b6934aa7..2cfa306112 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -570,7 +570,7 @@ public long ReplayAOF(long untilAddress = -1) /// public bool TryAddDatabase(int dbId, ref GarnetDatabase db) { - if (!allowMultiDb || !databases.TrySetValue(dbId, ref db)) + if ((!allowMultiDb && dbId != 0) || !databases.TrySetValue(dbId, ref db)) return false; HandleDatabaseAdded(dbId); @@ -587,7 +587,7 @@ public bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db) { db = default; - if (!allowMultiDb || !databases.TryGetOrSet(dbId, () => createDatabasesDelegate(dbId, out _, out _), out db, out var added)) + if ((!allowMultiDb && dbId != 0) || !databases.TryGetOrSet(dbId, () => createDatabasesDelegate(dbId, out _, out _), out db, out var added)) return false; if (added) @@ -967,6 +967,7 @@ internal void CommitAOF(bool spinWait) /// /// Wait for commits from all active databases /// + /// internal void WaitForCommit() { if (!serverOptions.EnableAOF || appendOnlyFile == null) return; @@ -1036,6 +1037,7 @@ internal async ValueTask CommitAOFAsync(int dbId = -1, CancellationToken token = var activeDbIdsSize = this.activeDbIdsLength; if (!allowMultiDb || activeDbIdsSize == 1 || dbId != -1) { + if (dbId == -1) dbId = 0; var dbFound = TryGetDatabase(dbId, out var db); Debug.Assert(dbFound); await db.AppendOnlyFile.CommitAsync(token: token); diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index 06d328f5d2..9888bb65e7 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -632,7 +632,7 @@ public void MultiDatabaseSaveRecoverByDbIdTest(bool backgroundSave) } expectedLastSave = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - Assert.That(lastSave, Is.InRange(expectedLastSave - 1, expectedLastSave)); + Assert.That(lastSave, Is.InRange(expectedLastSave - 2, expectedLastSave + 2)); // Verify DB 0 was not saved lastSaveStr = db1.Execute("LASTSAVE").ToString(); @@ -701,7 +701,7 @@ public void MultiDatabaseSaveRecoverByDbIdTest(bool backgroundSave) var prevLastSave = expectedLastSave; expectedLastSave = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - Assert.That(lastSave, Is.InRange(expectedLastSave - 1, expectedLastSave)); + Assert.That(lastSave, Is.InRange(expectedLastSave - 2, expectedLastSave + 2)); // Verify DB 1 was not saved Thread.Sleep(TimeSpan.FromSeconds(1)); From ab138ed810da5894de8686f06ac3c198641e7ebd Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 13 Feb 2025 20:42:20 -0800 Subject: [PATCH 49/82] Adding SPUBLISH, SSUBSCRIBE TO GarnetCommandsInfo.json --- libs/resources/RespCommandsInfo.json | 10 ++-- .../CommandInfoUpdater/CommandInfoUpdater.cs | 2 +- .../GarnetCommandsInfo.json | 50 +++++++++++++++++++ test/Garnet.test/RespCommandTests.cs | 3 +- 4 files changed, 58 insertions(+), 7 deletions(-) diff --git a/libs/resources/RespCommandsInfo.json b/libs/resources/RespCommandsInfo.json index e3f5de4b47..d74adfda93 100644 --- a/libs/resources/RespCommandsInfo.json +++ b/libs/resources/RespCommandsInfo.json @@ -4341,11 +4341,11 @@ "Command": "SPUBLISH", "Name": "SPUBLISH", "Arity": 3, - "Flags": "Fast, Loading, PubSub, Stale", + "Flags": "Loading, NoScript, PubSub, Stale", "FirstKey": 1, "LastKey": 1, "Step": 1, - "AclCategories": "Fast, PubSub", + "AclCategories": "PubSub, Read, Slow", "KeySpecifications": [ { "BeginSearch": { @@ -4358,7 +4358,7 @@ "KeyStep": 1, "Limit": 0 }, - "Flags": "NotKey" + "Flags": "RO" } ] }, @@ -4451,7 +4451,7 @@ "FirstKey": 1, "LastKey": -1, "Step": 1, - "AclCategories": "PubSub, Slow", + "AclCategories": "PubSub, Read, Slow", "KeySpecifications": [ { "BeginSearch": { @@ -4464,7 +4464,7 @@ "KeyStep": 1, "Limit": 0 }, - "Flags": "NotKey" + "Flags": "RO" } ] }, diff --git a/playground/CommandInfoUpdater/CommandInfoUpdater.cs b/playground/CommandInfoUpdater/CommandInfoUpdater.cs index ad1901c14a..fd0ab4238e 100644 --- a/playground/CommandInfoUpdater/CommandInfoUpdater.cs +++ b/playground/CommandInfoUpdater/CommandInfoUpdater.cs @@ -15,7 +15,7 @@ namespace CommandInfoUpdater /// public class CommandInfoUpdater { - const int QUERY_CMD_BATCH_SIZE = 25; + const int QUERY_CMD_BATCH_SIZE = 10; private static readonly string CommandInfoFileName = "RespCommandsInfo.json"; private static readonly string GarnetCommandInfoJsonPath = "GarnetCommandsInfo.json"; diff --git a/playground/CommandInfoUpdater/GarnetCommandsInfo.json b/playground/CommandInfoUpdater/GarnetCommandsInfo.json index ba349c0f18..1cc3b765dd 100644 --- a/playground/CommandInfoUpdater/GarnetCommandsInfo.json +++ b/playground/CommandInfoUpdater/GarnetCommandsInfo.json @@ -755,6 +755,56 @@ } ] }, + { + "Command": "SPUBLISH", + "Name": "SPUBLISH", + "Arity": 3, + "Flags": "Loading, NoScript, PubSub, Stale", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "PubSub, Slow, Read", + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Flags": "RO" + } + ] + }, + { + "Command": "SSUBSCRIBE", + "Name": "SSUBSCRIBE", + "Arity": -2, + "Flags": "Loading, NoScript, PubSub, Stale", + "FirstKey": 1, + "LastKey": -1, + "Step": 1, + "AclCategories": "PubSub, Slow, Read", + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": -1, + "KeyStep": 1, + "Limit": 0 + }, + "Flags": "RO" + } + ] + }, { "Command": "WATCH", "Name": "WATCH", diff --git a/test/Garnet.test/RespCommandTests.cs b/test/Garnet.test/RespCommandTests.cs index c5cc6f713e..757286fd6d 100644 --- a/test/Garnet.test/RespCommandTests.cs +++ b/test/Garnet.test/RespCommandTests.cs @@ -137,7 +137,8 @@ public void CommandsDocsCoverageTest() } var allCommands = Enum.GetValues().Except(noMetadataCommands).Except(internalOnlyCommands); - CollectionAssert.AreEquivalent(allCommands, commandsWithDocs, "Some commands have missing docs. Please see https://microsoft.github.io/garnet/docs/dev/garnet-api#adding-command-info for more details."); + Assert.That(commandsWithDocs, Is.SupersetOf(allCommands), + "Some commands have missing docs. Please see https://microsoft.github.io/garnet/docs/dev/garnet-api#adding-command-info for more details."); } /// From 923bafe8afbb5c62f8bb2f1470f837355592ce76 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 18 Feb 2025 16:04:57 -0800 Subject: [PATCH 50/82] wip --- libs/server/Resp/ArrayCommands.cs | 50 +++++++ libs/server/Resp/Parser/RespCommand.cs | 6 + libs/server/Resp/RespServerSession.cs | 22 +++ libs/server/StoreWrapper.cs | 129 +++++++++++------ .../CommandInfoUpdater/SupportedCommand.cs | 1 + test/Garnet.test/MultiDatabaseTests.cs | 134 +++++++++++++++++- test/Garnet.test/RespCommandTests.cs | 1 + 7 files changed, 296 insertions(+), 47 deletions(-) diff --git a/libs/server/Resp/ArrayCommands.cs b/libs/server/Resp/ArrayCommands.cs index a8196dfedc..47c4ff6187 100644 --- a/libs/server/Resp/ArrayCommands.cs +++ b/libs/server/Resp/ArrayCommands.cs @@ -262,6 +262,56 @@ private bool NetworkSELECT() return true; } + /// + /// SWAPDB + /// + /// + private bool NetworkSWAPDB() + { + if (parseState.Count != 2) + { + return AbortWithWrongNumberOfArguments(nameof(RespCommand.SWAPDB)); + } + + // Validate index + if (!parseState.TryGetInt(0, out var index1) || !parseState.TryGetInt(1, out var index2)) + { + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref dcurr, dend)) + SendAndReset(); + return true; + } + + if (index1 < storeWrapper.serverOptions.MaxDatabases && index2 < storeWrapper.serverOptions.MaxDatabases) + { + if (this.TrySwapDatabases(index1, index2)) + { + while (!RespWriteUtils.TryWriteDirect(CmdStrings.RESP_OK, ref dcurr, dend)) + SendAndReset(); + } + else + { + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_DB_INDEX_OUT_OF_RANGE, ref dcurr, dend)) + SendAndReset(); + } + } + else + { + if (storeWrapper.serverOptions.EnableCluster) + { + // Cluster mode does not allow DBID + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_SELECT_CLUSTER_MODE, ref dcurr, dend)) + SendAndReset(); + } + else + { + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_SELECT_INVALID_INDEX, ref dcurr, dend)) + SendAndReset(); + } + } + + return true; + } + private bool NetworkDBSIZE(ref TGarnetApi storageApi) where TGarnetApi : IGarnetApi { diff --git a/libs/server/Resp/Parser/RespCommand.cs b/libs/server/Resp/Parser/RespCommand.cs index d3640f1e7f..bb96e87c63 100644 --- a/libs/server/Resp/Parser/RespCommand.cs +++ b/libs/server/Resp/Parser/RespCommand.cs @@ -263,6 +263,7 @@ public enum RespCommand : ushort FORCEGC, PURGEBP, FAILOVER, + SWAPDB, // Custom commands CustomTxn, @@ -383,6 +384,7 @@ public static class RespCommandExtensions RespCommand.ASYNC, RespCommand.PING, RespCommand.SELECT, + RespCommand.SWAPDB, RespCommand.ECHO, RespCommand.MONITOR, RespCommand.MODULE_LOADCS, @@ -1266,6 +1268,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan } } } + else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("SWAPDB\r\n"u8)) + { + return RespCommand.SWAPDB; + } break; case 'U': diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index ae4c66da74..d412aff0b5 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -655,6 +655,7 @@ private bool ProcessArrayCommands(RespCommand cmd, ref TGarnetApi st RespCommand.MSETNX => NetworkMSETNX(ref storageApi), RespCommand.UNLINK => NetworkDEL(ref storageApi), RespCommand.SELECT => NetworkSELECT(), + RespCommand.SWAPDB => NetworkSWAPDB(), RespCommand.WATCH => NetworkWATCH(), RespCommand.WATCHMS => NetworkWATCH_MS(), RespCommand.WATCHOS => NetworkWATCH_OS(), @@ -1274,6 +1275,27 @@ internal bool TrySwitchActiveDatabaseSession(int dbId) return true; } + internal bool TrySwapDatabases(int dbId1, int dbId2) + { + if (!allowMultiDb) return false; + if (dbId1 == dbId2) return true; + if (!storeWrapper.TrySwapDatabases(dbId1, dbId2)) return false; + + if (!databaseSessions.TryGetOrSet(dbId1, () => CreateDatabaseSession(dbId1), out var dbSession1, out _) || + !databaseSessions.TryGetOrSet(dbId2, () => CreateDatabaseSession(dbId2), out var dbSession2, out _)) + return false; + + databaseSessions.Map[dbId1] = dbSession2; + databaseSessions.Map[dbId2] = dbSession1; + + if (activeDbId == dbId1) + SwitchActiveDatabaseSession(dbId1, ref dbSession2); + else if (activeDbId == dbId2) + SwitchActiveDatabaseSession(dbId2, ref dbSession1); + + return true; + } + private void SwitchActiveDatabaseSession(int dbId, ref GarnetDatabaseSession dbSession) { this.activeDbId = dbId; diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 2cfa306112..7f90a2a522 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -15,7 +15,6 @@ using Garnet.server.Auth.Settings; using Microsoft.Extensions.Logging; using Tsavorite.core; -using IStreamProvider = Garnet.common.IStreamProvider; namespace Garnet.server { @@ -35,35 +34,40 @@ public sealed class StoreWrapper readonly IGarnetServer server; internal readonly long startupTime; + /// + /// Reference to default database (DB 0) + /// + public ref GarnetDatabase DefaultDatabase => ref databases.Map[0]; + /// /// Store (of DB 0) /// - public TsavoriteKV store; + public TsavoriteKV store => DefaultDatabase.MainStore; /// /// Object store (of DB 0) /// - public TsavoriteKV objectStore; + public TsavoriteKV objectStore => DefaultDatabase.ObjectStore; /// /// AOF (of DB 0) /// - public TsavoriteLog appendOnlyFile; + public TsavoriteLog appendOnlyFile => DefaultDatabase.AppendOnlyFile; /// /// Last save time (of DB 0) /// - public DateTimeOffset lastSaveTime; + public DateTimeOffset lastSaveTime => DefaultDatabase.LastSaveTime; /// /// Object store size tracker (of DB 0) /// - public CacheSizeTracker objectStoreSizeTracker; + public CacheSizeTracker objectStoreSizeTracker => DefaultDatabase.ObjectStoreSizeTracker; /// /// Version map (of DB 0) /// - internal WatchVersionMap versionMap; + internal WatchVersionMap versionMap => DefaultDatabase.VersionMap; /// /// Server options @@ -138,6 +142,7 @@ public sealed class StoreWrapper readonly CancellationTokenSource ctsCommit; SingleWriterMultiReaderLock checkpointTaskLock; + SingleWriterMultiReaderLock swapDbsLock; // True if this server supports more than one logical database readonly bool allowMultiDb; @@ -173,9 +178,6 @@ public sealed class StoreWrapper readonly string checkpointDirBaseName; - // Stream provider for serializing DB IDs file - readonly IStreamProvider databaseIdsStreamProvider; - // True if StoreWrapper instance is disposed bool disposed = false; @@ -201,7 +203,6 @@ public StoreWrapper( this.startupTime = DateTimeOffset.UtcNow.Ticks; this.serverOptions = serverOptions; this.subscribeBroker = subscribeBroker; - this.lastSaveTime = DateTimeOffset.FromUnixTimeSeconds(0); this.customCommandManager = customCommandManager; this.createDatabasesDelegate = createsDatabaseDelegate; this.monitor = serverOptions.MetricsSamplingFrequency > 0 @@ -240,13 +241,6 @@ public StoreWrapper( // Set new database in map if (!this.TryAddDatabase(0, ref db)) throw new GarnetException("Failed to set initial database in databases map"); - - this.InitializeFieldsFromDatabase(databases.Map[0]); - } - - if (allowMultiDb) - { - databaseIdsStreamProvider = serverOptions.StreamProviderCreator(); } if (serverOptions.SlowLogThreshold > 0) @@ -406,7 +400,7 @@ public void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStore } if (storeVersion > 0 || objectStoreVersion > 0) - lastSaveTime = DateTimeOffset.UtcNow; + DefaultDatabase.LastSaveTime = DateTimeOffset.UtcNow; } else { @@ -452,7 +446,7 @@ private void RecoverDatabaseCheckpoint(int dbId) var lastSave = DateTimeOffset.UtcNow; db.LastSaveTime = lastSave; if (dbId == 0) - lastSaveTime = lastSave; + DefaultDatabase.LastSaveTime = lastSave; var databasesMapSnapshot = databases.Map; databasesMapSnapshot[dbId] = db; @@ -496,7 +490,7 @@ public void Reset(int dbId = 0) { try { - var db = databases.Map[dbId]; + ref var db = ref databases.Map[dbId]; if (db.MainStore.Log.TailAddress > 64) db.MainStore.Reset(); if (db.ObjectStore?.Log.TailAddress > 64) @@ -505,7 +499,7 @@ public void Reset(int dbId = 0) var lastSave = DateTimeOffset.FromUnixTimeSeconds(0); if (dbId == 0) - lastSaveTime = lastSave; + DefaultDatabase.LastSaveTime = lastSave; db.LastSaveTime = lastSave; } catch (Exception ex) @@ -546,7 +540,7 @@ public long ReplayAOF(long untilAddress = -1) if (dbId == 0) { replicationOffset = aofProcessor.ReplicationOffset; - this.lastSaveTime = lastSave; + DefaultDatabase.LastSaveTime = lastSave; } } @@ -596,6 +590,35 @@ public bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db) return true; } + /// + /// Try to swap between two database instances + /// + /// First database ID + /// Second database ID + /// True if swap successful + public bool TrySwapDatabases(int dbId1, int dbId2) + { + if (!allowMultiDb) return false; + + if (!this.TryGetOrAddDatabase(dbId1, out var db1) || + !this.TryGetOrAddDatabase(dbId2, out var db2)) + return false; + + swapDbsLock.WriteLock(); + try + { + var databaseMapSnapshot = this.databases.Map; + databaseMapSnapshot[dbId2] = db1; + databaseMapSnapshot[dbId1] = db2; + } + finally + { + swapDbsLock.WriteUnlock(); + } + + return true; + } + async Task AutoCheckpointBasedOnAofSizeLimit(long aofSizeLimit, CancellationToken token = default, ILogger logger = null) { try @@ -710,6 +733,16 @@ void MultiDatabaseCommit(ref Task[] tasks, CancellationToken token, bool spinWai tasks ??= new Task[activeDbIdsSize]; + // Take a read lock to make sure that swap-db operation is not in progress + var lockAcquired = false; + while (!lockAcquired && !token.IsCancellationRequested && !disposed) + { + Task.Yield(); + lockAcquired = swapDbsLock.TryReadLock(); + } + + if (token.IsCancellationRequested || disposed) return; + for (var i = 0; i < activeDbIdsSize; i++) { var dbId = activeDbIdsSnapshot[i]; @@ -719,10 +752,12 @@ void MultiDatabaseCommit(ref Task[] tasks, CancellationToken token, bool spinWai tasks[i] = db.AppendOnlyFile.CommitAsync(null, token).AsTask(); } + var completion = Task.WhenAll(tasks).ContinueWith(_ => swapDbsLock.ReadUnlock(), token); + if (!spinWait) return; - Task.WhenAll(tasks).Wait(token); + completion.Wait(token); } async Task CompactionTask(int compactionFrequencySecs, CancellationToken token = default) @@ -979,6 +1014,16 @@ internal void WaitForCommit() return; } + // Take a read lock to make sure that swap-db operation is not in progress + var lockAcquired = false; + while (!lockAcquired && !disposed) + { + Task.Yield(); + lockAcquired = swapDbsLock.TryReadLock(); + } + + if (disposed) return; + var databasesMapSnapshot = databases.Map; var activeDbIdsSnapshot = activeDbIds; @@ -990,7 +1035,7 @@ internal void WaitForCommit() tasks[i] = db.AppendOnlyFile.WaitForCommitAsync().AsTask(); } - Task.WaitAll(tasks); + Task.WhenAll(tasks).ContinueWith(_ => swapDbsLock.ReadUnlock()).Wait(); } /// @@ -1009,6 +1054,16 @@ internal async ValueTask WaitForCommitAsync(CancellationToken token = default) return; } + // Take a read lock to make sure that swap-db operation is not in progress + var lockAcquired = false; + while (!lockAcquired && !token.IsCancellationRequested && !disposed) + { + await Task.Yield(); + lockAcquired = swapDbsLock.TryReadLock(); + } + + if (token.IsCancellationRequested || disposed) return; + var databasesMapSnapshot = databases.Map; var activeDbIdsSnapshot = activeDbIds; @@ -1020,7 +1075,7 @@ internal async ValueTask WaitForCommitAsync(CancellationToken token = default) tasks[i] = db.AppendOnlyFile.WaitForCommitAsync(token: token).AsTask(); } - await Task.WhenAll(tasks); + await Task.WhenAll(tasks).ContinueWith(_ => swapDbsLock.ReadUnlock(), token); } /// @@ -1429,7 +1484,7 @@ private async Task CheckpointDatabaseTask(int dbId, StoreType storeType, ILogger var lastSave = DateTimeOffset.UtcNow; if (dbId == 0) - lastSaveTime = lastSave; + DefaultDatabase.LastSaveTime = lastSave; db.LastSaveTime = lastSave; databasesMapSnapshot[dbId] = db; } @@ -1618,22 +1673,6 @@ private void CopyDatabases(StoreWrapper src, bool recordToAof) recordToAof ? db.AofDevice : null, recordToAof ? db.AppendOnlyFile : null); this.TryAddDatabase(dbId, ref dbCopy); } - - InitializeFieldsFromDatabase(databases.Map[0]); - } - - /// - /// Initializes default fields to those of a specified database (i.e. DB 0) - /// - /// Input database - private void InitializeFieldsFromDatabase(GarnetDatabase db) - { - // Set fields to default database - this.store = db.MainStore; - this.objectStore = db.ObjectStore; - this.objectStoreSizeTracker = db.ObjectStoreSizeTracker; - this.appendOnlyFile = db.AppendOnlyFile; - this.versionMap = db.VersionMap; } /// @@ -1718,10 +1757,10 @@ private void HandleDatabaseAdded(int dbId) // Set the last added ID activeDbIdsUpdated[dbIdIdx] = dbId; - checkpointTasks = new Task[newSize]; - aofTasks = new Task[newSize]; activeDbIds = activeDbIdsUpdated; activeDbIdsLength = dbIdIdx + 1; + checkpointTasks = new Task[activeDbIdsLength]; + aofTasks = new Task[activeDbIdsLength]; } } } diff --git a/playground/CommandInfoUpdater/SupportedCommand.cs b/playground/CommandInfoUpdater/SupportedCommand.cs index 9f2341398d..45644131d1 100644 --- a/playground/CommandInfoUpdater/SupportedCommand.cs +++ b/playground/CommandInfoUpdater/SupportedCommand.cs @@ -289,6 +289,7 @@ public class SupportedCommand new("SINTER", RespCommand.SINTER), new("SINTERCARD", RespCommand.SINTERCARD), new("SINTERSTORE", RespCommand.SINTERSTORE), + new("SWAPDB", RespCommand.SWAPDB), new("TIME", RespCommand.TIME), new("TTL", RespCommand.TTL), new("TYPE", RespCommand.TYPE), diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index 9888bb65e7..eb9b152919 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -4,8 +4,6 @@ using System.Threading; using System.Threading.Tasks; using Garnet.common; -using Garnet.server; -using Newtonsoft.Json.Linq; using NUnit.Framework; using NUnit.Framework.Legacy; using StackExchange.Redis; @@ -239,6 +237,138 @@ public void MultiDatabaseBasicSelectTestLC() ClassicAssert.AreEqual(expectedResponse, actualValue); } + [Test] + public void MultiDatabaseSwapDatabasesTestLC() + { + var db1Key1 = "db1:key1"; + var db1Key2 = "db1:key2"; + var db2Key1 = "db2:key1"; + var db2Key2 = "db2:key2"; + var db12Key1 = "db12:key1"; + var db12Key2 = "db12:key2"; + + using var lightClientRequest = TestUtils.CreateRequest(); + + // Add data to DB 0 + var response = lightClientRequest.SendCommand($"SET {db1Key1} db1:value1"); + var expectedResponse = "+OK\r\n"; + var actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand($"LPUSH {db1Key2} db1:val1 db1:val2"); + expectedResponse = ":2\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + // Add data to DB 1 + lightClientRequest.SendCommand($"SELECT 1"); + expectedResponse = "+OK\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand($"SET {db2Key1} db2:value1"); + expectedResponse = "+OK\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand($"SADD {db2Key2} db2:val1 db2:val2"); + expectedResponse = ":2\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + // Add data to DB 11 + lightClientRequest.SendCommand($"SELECT 11"); + expectedResponse = "+OK\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand($"SET {db12Key1} db12:value1"); + expectedResponse = "+OK\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand($"SADD {db12Key2} db12:val1 db12:val2"); + expectedResponse = ":2\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + // Swap DB 1 AND DB 11 (from DB 11 context) + lightClientRequest.SendCommand($"SWAPDB 1 11"); + expectedResponse = "+OK\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + // Verify data in DB 11 is previous data from DB 1 + response = lightClientRequest.SendCommand($"GET {db2Key1}", 2); + expectedResponse = "$10\r\ndb2:value1\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand($"SISMEMBER {db2Key2} db2:val2"); + expectedResponse = ":1\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + // Verify data in DB 1 is previous data from DB 11 + lightClientRequest.SendCommand($"SELECT 1"); + expectedResponse = "+OK\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand($"GET {db12Key1}", 2); + expectedResponse = "$11\r\ndb12:value1\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand($"SISMEMBER {db12Key2} db12:val2"); + expectedResponse = ":1\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + // Swap DB 11 AND DB 0 (from DB 1 context) + lightClientRequest.SendCommand($"SELECT 1"); + expectedResponse = "+OK\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + lightClientRequest.SendCommand($"SWAPDB 11 0"); + expectedResponse = "+OK\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + // Verify data in DB 0 is previous data from DB 11 + lightClientRequest.SendCommand($"SELECT 0"); + expectedResponse = "+OK\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand($"GET {db2Key1}", 2); + expectedResponse = "$10\r\ndb2:value1\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand($"SISMEMBER {db2Key2} db2:val2"); + expectedResponse = ":1\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + // Verify data in DB 11 is previous data from DB 0 + lightClientRequest.SendCommand($"SELECT 11"); + expectedResponse = "+OK\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand($"GET {db1Key1}", 2); + expectedResponse = "$10\r\ndb1:value1\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand($"LPOP {db1Key2}", 2); + expectedResponse = "$8\r\ndb1:val2\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + } + [Test] [Ignore("")] public void MultiDatabaseSelectMultithreadedTestLC() diff --git a/test/Garnet.test/RespCommandTests.cs b/test/Garnet.test/RespCommandTests.cs index 757286fd6d..bb8fd93085 100644 --- a/test/Garnet.test/RespCommandTests.cs +++ b/test/Garnet.test/RespCommandTests.cs @@ -427,6 +427,7 @@ public void AofIndependentCommandsTest() RespCommand.ASYNC, RespCommand.PING, RespCommand.SELECT, + RespCommand.SWAPDB, RespCommand.ECHO, RespCommand.MONITOR, RespCommand.MODULE_LOADCS, From 8ab2775913e2a6bc661381e235abdc7431b4ee7d Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 18 Feb 2025 17:09:08 -0800 Subject: [PATCH 51/82] wip --- libs/host/GarnetServer.cs | 8 +- libs/server/GarnetDatabase.cs | 9 +- libs/server/StoreWrapper.cs | 294 ++++++++++++++++++++-------------- 3 files changed, 191 insertions(+), 120 deletions(-) diff --git a/libs/host/GarnetServer.cs b/libs/host/GarnetServer.cs index a330f28eeb..93e6af035c 100644 --- a/libs/host/GarnetServer.cs +++ b/libs/host/GarnetServer.cs @@ -224,7 +224,7 @@ private void InitializeServer() throw new Exception($"Unable to call ThreadPool.SetMaxThreads with {maxThreads}, {maxCPThreads}"); StoreWrapper.DatabaseCreatorDelegate createDatabaseDelegate = (int dbId, out string storeCheckpointDir, out string aofDir) => - CreateDatabase(dbId, clusterFactory, customCommandManager, out storeCheckpointDir, out aofDir); + CreateDatabase(dbId, opts, clusterFactory, customCommandManager, out storeCheckpointDir, out aofDir); if (!opts.DisablePubSub) subscribeBroker = new SubscribeBroker(null, opts.PubSubPageSizeBytes(), opts.SubscriberRefreshFrequencyMs, true, logger); @@ -272,14 +272,16 @@ private void InitializeServer() LoadModules(customCommandManager); } - private GarnetDatabase CreateDatabase(int dbId, ClusterFactory clusterFactory, + private GarnetDatabase CreateDatabase(int dbId, GarnetServerOptions serverOptions, ClusterFactory clusterFactory, CustomCommandManager customCommandManager, out string storeCheckpointDir, out string aofDir) { var store = CreateMainStore(dbId, clusterFactory, out var checkpointDir, out storeCheckpointDir); var objectStore = CreateObjectStore(dbId, clusterFactory, customCommandManager, checkpointDir, out var objectStoreSizeTracker); var (aofDevice, aof) = CreateAOF(dbId, out aofDir); - return new GarnetDatabase(store, objectStore, objectStoreSizeTracker, aofDevice, aof); + return new GarnetDatabase(store, objectStore, objectStoreSizeTracker, aofDevice, aof, + serverOptions.AdjustedIndexMaxCacheLines == 0, + serverOptions.AdjustedObjectStoreIndexMaxCacheLines == 0); } private void LoadModules(CustomCommandManager customCommandManager) diff --git a/libs/server/GarnetDatabase.cs b/libs/server/GarnetDatabase.cs index 78be12b957..d77b7c84f8 100644 --- a/libs/server/GarnetDatabase.cs +++ b/libs/server/GarnetDatabase.cs @@ -66,11 +66,16 @@ public struct GarnetDatabase : IDisposable /// public DateTimeOffset LastSaveTime; + public bool MainStoreIndexMaxedOut; + + public bool ObjectStoreIndexMaxedOut; + bool disposed = false; public GarnetDatabase(TsavoriteKV mainStore, TsavoriteKV objectStore, - CacheSizeTracker objectStoreSizeTracker, IDevice aofDevice, TsavoriteLog appendOnlyFile) + CacheSizeTracker objectStoreSizeTracker, IDevice aofDevice, TsavoriteLog appendOnlyFile, + bool mainStoreIndexMaxedOut, bool objectStoreIndexMaxedOut) { MainStore = mainStore; ObjectStore = objectStore; @@ -81,6 +86,8 @@ public GarnetDatabase(TsavoriteKV MainStore == null; diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 7f90a2a522..56b20f9ec2 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -142,7 +142,7 @@ public sealed class StoreWrapper readonly CancellationTokenSource ctsCommit; SingleWriterMultiReaderLock checkpointTaskLock; - SingleWriterMultiReaderLock swapDbsLock; + SingleWriterMultiReaderLock databasesLock; // True if this server supports more than one logical database readonly bool allowMultiDb; @@ -604,7 +604,7 @@ public bool TrySwapDatabases(int dbId1, int dbId2) !this.TryGetOrAddDatabase(dbId2, out var db2)) return false; - swapDbsLock.WriteLock(); + databasesLock.WriteLock(); try { var databaseMapSnapshot = this.databases.Map; @@ -613,7 +613,7 @@ public bool TrySwapDatabases(int dbId1, int dbId2) } finally { - swapDbsLock.WriteUnlock(); + databasesLock.WriteUnlock(); } return true; @@ -648,31 +648,41 @@ async Task AutoCheckpointBasedOnAofSizeLimit(long aofSizeLimit, CancellationToke return; } - var databasesMapSnapshot = databases.Map; - var activeDbIdsSnapshot = activeDbIds; - - if (dbIdsToCheckpoint == null || dbIdsToCheckpoint.Length < activeDbIdsSize) - dbIdsToCheckpoint = new int[activeDbIdsSize]; + var lockAcquired = TryGetDatabasesReadLockAsync(token).Result; + if (!lockAcquired) continue; - var dbIdsIdx = 0; - for (var i = 0; i < activeDbIdsSize; i++) + try { - var dbId = activeDbIdsSnapshot[i]; - var db = databasesMapSnapshot[dbId]; - Debug.Assert(!db.IsDefault()); + var databasesMapSnapshot = databases.Map; + var activeDbIdsSnapshot = activeDbIds; - var dbAofSize = db.AppendOnlyFile.TailAddress - db.AppendOnlyFile.BeginAddress; - if (dbAofSize > aofSizeLimit) + if (dbIdsToCheckpoint == null || dbIdsToCheckpoint.Length < activeDbIdsSize) + dbIdsToCheckpoint = new int[activeDbIdsSize]; + + var dbIdsIdx = 0; + for (var i = 0; i < activeDbIdsSize; i++) { - dbIdsToCheckpoint[dbIdsIdx++] = dbId; - break; + var dbId = activeDbIdsSnapshot[i]; + var db = databasesMapSnapshot[dbId]; + Debug.Assert(!db.IsDefault()); + + var dbAofSize = db.AppendOnlyFile.TailAddress - db.AppendOnlyFile.BeginAddress; + if (dbAofSize > aofSizeLimit) + { + dbIdsToCheckpoint[dbIdsIdx++] = dbId; + break; + } } - } - if (dbIdsIdx > 0) + if (dbIdsIdx > 0) + { + logger?.LogInformation("Enforcing AOF size limit currentAofSize: {currAofSize} > AofSizeLimit: {AofSizeLimit}", aofSizeAtLimit, aofSizeLimit); + CheckpointDatabases(StoreType.All, ref dbIdsToCheckpoint, ref checkpointTasks, logger: logger, token: token); + } + } + finally { - logger?.LogInformation("Enforcing AOF size limit currentAofSize: {currAofSize} > AofSizeLimit: {AofSizeLimit}", aofSizeAtLimit, aofSizeLimit); - CheckpointDatabases(StoreType.All, ref dbIdsToCheckpoint, ref checkpointTasks, logger: logger, token: token); + databasesLock.ReadUnlock(); } } } @@ -734,14 +744,8 @@ void MultiDatabaseCommit(ref Task[] tasks, CancellationToken token, bool spinWai tasks ??= new Task[activeDbIdsSize]; // Take a read lock to make sure that swap-db operation is not in progress - var lockAcquired = false; - while (!lockAcquired && !token.IsCancellationRequested && !disposed) - { - Task.Yield(); - lockAcquired = swapDbsLock.TryReadLock(); - } - - if (token.IsCancellationRequested || disposed) return; + var lockAcquired = TryGetDatabasesReadLockAsync(token).Result; + if (!lockAcquired) return; for (var i = 0; i < activeDbIdsSize; i++) { @@ -752,7 +756,7 @@ void MultiDatabaseCommit(ref Task[] tasks, CancellationToken token, bool spinWai tasks[i] = db.AppendOnlyFile.CommitAsync(null, token).AsTask(); } - var completion = Task.WhenAll(tasks).ContinueWith(_ => swapDbsLock.ReadUnlock(), token); + var completion = Task.WhenAll(tasks).ContinueWith(_ => databasesLock.ReadUnlock(), token); if (!spinWait) return; @@ -769,18 +773,28 @@ async Task CompactionTask(int compactionFrequencySecs, CancellationToken token = { if (token.IsCancellationRequested) return; - var databasesMapSnapshot = databases.Map; - - var activeDbIdsSize = activeDbIdsLength; - var activeDbIdsSnapshot = activeDbIds; + var lockAcquired = TryGetDatabasesReadLockAsync(token).Result; + if (!lockAcquired) continue; - for (var i = 0; i < activeDbIdsSize; i++) + try { - var dbId = activeDbIdsSnapshot[i]; - var db = databasesMapSnapshot[dbId]; - Debug.Assert(!db.IsDefault()); + var databasesMapSnapshot = databases.Map; - DoCompaction(ref db, serverOptions.CompactionMaxSegments, serverOptions.ObjectStoreCompactionMaxSegments, 1, serverOptions.CompactionType, serverOptions.CompactionForceDelete); + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + + for (var i = 0; i < activeDbIdsSize; i++) + { + var dbId = activeDbIdsSnapshot[i]; + var db = databasesMapSnapshot[dbId]; + Debug.Assert(!db.IsDefault()); + + DoCompaction(ref db, serverOptions.CompactionMaxSegments, serverOptions.ObjectStoreCompactionMaxSegments, 1, serverOptions.CompactionType, serverOptions.CompactionForceDelete); + } + } + finally + { + databasesLock.ReadUnlock(); } if (!serverOptions.CompactionForceDelete) @@ -1015,14 +1029,8 @@ internal void WaitForCommit() } // Take a read lock to make sure that swap-db operation is not in progress - var lockAcquired = false; - while (!lockAcquired && !disposed) - { - Task.Yield(); - lockAcquired = swapDbsLock.TryReadLock(); - } - - if (disposed) return; + var lockAcquired = TryGetDatabasesReadLockAsync().Result; + if (!lockAcquired) return; var databasesMapSnapshot = databases.Map; var activeDbIdsSnapshot = activeDbIds; @@ -1035,7 +1043,7 @@ internal void WaitForCommit() tasks[i] = db.AppendOnlyFile.WaitForCommitAsync().AsTask(); } - Task.WhenAll(tasks).ContinueWith(_ => swapDbsLock.ReadUnlock()).Wait(); + Task.WhenAll(tasks).ContinueWith(_ => databasesLock.ReadUnlock()).Wait(); } /// @@ -1055,14 +1063,8 @@ internal async ValueTask WaitForCommitAsync(CancellationToken token = default) } // Take a read lock to make sure that swap-db operation is not in progress - var lockAcquired = false; - while (!lockAcquired && !token.IsCancellationRequested && !disposed) - { - await Task.Yield(); - lockAcquired = swapDbsLock.TryReadLock(); - } - - if (token.IsCancellationRequested || disposed) return; + var lockAcquired = TryGetDatabasesReadLockAsync(token).Result; + if (!lockAcquired) return; var databasesMapSnapshot = databases.Map; var activeDbIdsSnapshot = activeDbIds; @@ -1075,7 +1077,7 @@ internal async ValueTask WaitForCommitAsync(CancellationToken token = default) tasks[i] = db.AppendOnlyFile.WaitForCommitAsync(token: token).AsTask(); } - await Task.WhenAll(tasks).ContinueWith(_ => swapDbsLock.ReadUnlock(), token); + await Task.WhenAll(tasks).ContinueWith(_ => databasesLock.ReadUnlock(), token); } /// @@ -1099,6 +1101,9 @@ internal async ValueTask CommitAOFAsync(int dbId = -1, CancellationToken token = return; } + var lockAcquired = TryGetDatabasesReadLockAsync(token).Result; + if (!lockAcquired) return; + var databasesMapSnapshot = databases.Map; var activeDbIdsSnapshot = activeDbIds; @@ -1110,7 +1115,7 @@ internal async ValueTask CommitAOFAsync(int dbId = -1, CancellationToken token = tasks[i] = db.AppendOnlyFile.CommitAsync(token: token).AsTask(); } - await Task.WhenAll(tasks); + await Task.WhenAll(tasks).ContinueWith(_ => databasesLock.ReadUnlock(), token); } internal void Start() @@ -1168,10 +1173,6 @@ private async void IndexAutoGrowTask(CancellationToken token) { try { - var databaseDataInitialized = new bool[serverOptions.MaxDatabases]; - var databaseMainStoreIndexMaxedOut = new bool[serverOptions.MaxDatabases]; - var databaseObjectStoreIndexMaxedOut = new bool[serverOptions.MaxDatabases]; - var allIndexesMaxedOut = false; while (!allIndexesMaxedOut) @@ -1182,45 +1183,65 @@ private async void IndexAutoGrowTask(CancellationToken token) await Task.Delay(TimeSpan.FromSeconds(serverOptions.IndexResizeFrequencySecs), token); - var databasesMapSnapshot = databases.Map; - - var activeDbIdsSize = activeDbIdsLength; - var activeDbIdsSnapshot = activeDbIds; + var lockAcquired = TryGetDatabasesReadLockAsync(token).Result; + if (!lockAcquired) return; - for (var i = 0; i < activeDbIdsSize; i++) + try { - var dbId = activeDbIdsSnapshot[i]; - if (!databaseDataInitialized[dbId]) - { - Debug.Assert(!databasesMapSnapshot[dbId].IsDefault()); - databaseMainStoreIndexMaxedOut[dbId] = serverOptions.AdjustedIndexMaxCacheLines == 0; - databaseObjectStoreIndexMaxedOut[dbId] = serverOptions.AdjustedObjectStoreIndexMaxCacheLines == 0; - databaseDataInitialized[dbId] = true; - } + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; - if (!databaseMainStoreIndexMaxedOut[dbId]) - { - var dbMainStore = databasesMapSnapshot[dbId].MainStore; - databaseMainStoreIndexMaxedOut[dbId] = GrowIndexIfNeeded(StoreType.Main, - serverOptions.AdjustedIndexMaxCacheLines, dbMainStore.OverflowBucketAllocations, - () => dbMainStore.IndexSize, () => dbMainStore.GrowIndex()); - - if (!databaseMainStoreIndexMaxedOut[dbId]) - allIndexesMaxedOut = false; - } + var databasesMapSnapshot = databases.Map; - if (!databaseObjectStoreIndexMaxedOut[dbId]) + for (var i = 0; i < activeDbIdsSize; i++) { - var dbObjectStore = databasesMapSnapshot[dbId].ObjectStore; - databaseObjectStoreIndexMaxedOut[dbId] = GrowIndexIfNeeded(StoreType.Object, - serverOptions.AdjustedObjectStoreIndexMaxCacheLines, - dbObjectStore.OverflowBucketAllocations, - () => dbObjectStore.IndexSize, () => dbObjectStore.GrowIndex()); - - if (!databaseObjectStoreIndexMaxedOut[dbId]) - allIndexesMaxedOut = false; + var dbId = activeDbIdsSnapshot[i]; + var db = databasesMapSnapshot[dbId]; + var dbUpdated = false; + + if (!db.MainStoreIndexMaxedOut) + { + var dbMainStore = databasesMapSnapshot[dbId].MainStore; + if (GrowIndexIfNeeded(StoreType.Main, + serverOptions.AdjustedIndexMaxCacheLines, dbMainStore.OverflowBucketAllocations, + () => dbMainStore.IndexSize, () => dbMainStore.GrowIndex())) + { + db.MainStoreIndexMaxedOut = true; + dbUpdated = true; + } + else + { + allIndexesMaxedOut = false; + } + } + + if (!db.ObjectStoreIndexMaxedOut) + { + var dbObjectStore = databasesMapSnapshot[dbId].ObjectStore; + if (GrowIndexIfNeeded(StoreType.Object, + serverOptions.AdjustedObjectStoreIndexMaxCacheLines, + dbObjectStore.OverflowBucketAllocations, + () => dbObjectStore.IndexSize, () => dbObjectStore.GrowIndex())) + { + db.ObjectStoreIndexMaxedOut = true; + dbUpdated = true; + } + else + { + allIndexesMaxedOut = false; + } + } + + if (dbUpdated) + { + databasesMapSnapshot[dbId] = db; + } } } + finally + { + databasesLock.ReadUnlock(); + } } } catch (Exception ex) @@ -1291,12 +1312,48 @@ public void Dispose() public bool TryPauseCheckpoints() => checkpointTaskLock.TryWriteLock(); + /// + /// Continuously try to take a lock for checkpointing until acquired or token was cancelled + /// + /// Cancellation token + /// True if lock acquired + public async Task TryPauseCheckpointsContinuousAsync(CancellationToken token = default) + { + var checkpointsPaused = TryPauseCheckpoints(); + + while (!checkpointsPaused && !token.IsCancellationRequested && !disposed) + { + await Task.Yield(); + checkpointsPaused = TryPauseCheckpoints(); + } + + return checkpointsPaused; + } + /// /// Release checkpoint task lock /// public void ResumeCheckpoints() => checkpointTaskLock.WriteUnlock(); + /// + /// Continuously try to take a database read lock that ensures no db swap operations occur + /// + /// Cancellation token + /// True if lock acquired + public async Task TryGetDatabasesReadLockAsync(CancellationToken token = default) + { + var lockAcquired = databasesLock.TryReadLock(); + + while (!lockAcquired && !token.IsCancellationRequested && !disposed) + { + await Task.Yield(); + lockAcquired = databasesLock.TryReadLock(); + } + + return lockAcquired; + } + /// /// Take a checkpoint if no checkpoint was taken after the provided time offset /// @@ -1306,23 +1363,18 @@ public void ResumeCheckpoints() public async Task TakeOnDemandCheckpoint(DateTimeOffset entryTime, int dbId = 0) { // Take lock to ensure no other task will be taking a checkpoint - var lockAcquired = TryPauseCheckpoints(); - while (!lockAcquired && !disposed) - { - await Task.Yield(); - lockAcquired = TryPauseCheckpoints(); - } + var checkpointsPaused = TryPauseCheckpoints(); // If an external task has taken a checkpoint beyond the provided entryTime return - if (disposed || databases.Map[dbId].LastSaveTime > entryTime) + if (!checkpointsPaused || databases.Map[dbId].LastSaveTime > entryTime) { - if (lockAcquired) + if (checkpointsPaused) ResumeCheckpoints(); return; } // Necessary to take a checkpoint because the latest checkpoint is before entryTime - await CheckpointTask(StoreType.All, dbId, lockAcquired: true, logger: logger); + await CheckpointTask(StoreType.All, dbId, checkpointsPaused: true, logger: logger); } /// @@ -1349,10 +1401,20 @@ public bool TakeCheckpoint(bool background, StoreType storeType = StoreType.All, return true; } - var activeDbIdsSnapshot = activeDbIds; + var lockAcquired = TryGetDatabasesReadLockAsync(token).Result; + if (!lockAcquired) return false; - var tasks = new Task[activeDbIdsSize]; - return CheckpointDatabases(storeType, ref activeDbIdsSnapshot, ref tasks, logger: logger, token: token); + try + { + var activeDbIdsSnapshot = activeDbIds; + + var tasks = new Task[activeDbIdsSize]; + return CheckpointDatabases(storeType, ref activeDbIdsSnapshot, ref tasks, logger: logger, token: token); + } + finally + { + databasesLock.ReadUnlock(); + } } /// @@ -1360,22 +1422,21 @@ public bool TakeCheckpoint(bool background, StoreType storeType = StoreType.All, /// /// Store type to checkpoint /// ID of database to checkpoint (default: DB 0) - /// True if lock previously acquired + /// True if lock previously acquired /// Logger /// Cancellation token /// Task - private async Task CheckpointTask(StoreType storeType, int dbId = 0, bool lockAcquired = false, ILogger logger = null, CancellationToken token = default) + private async Task CheckpointTask(StoreType storeType, int dbId = 0, bool checkpointsPaused = false, ILogger logger = null, CancellationToken token = default) { try { - // Take lock to ensure no other task will be taking a checkpoint - while (!lockAcquired && !token.IsCancellationRequested && !disposed) + if (!checkpointsPaused) { - await Task.Yield(); - lockAcquired = TryPauseCheckpoints(); - } + // Take lock to ensure no other task will be taking a checkpoint + checkpointsPaused = await TryPauseCheckpointsContinuousAsync(token); - if (token.IsCancellationRequested || disposed) return; + if (!checkpointsPaused) return; + } await CheckpointDatabaseTask(dbId, storeType, logger); } @@ -1385,7 +1446,7 @@ private async Task CheckpointTask(StoreType storeType, int dbId = 0, bool lockAc } finally { - if (lockAcquired) + if (checkpointsPaused) ResumeCheckpoints(); } } @@ -1670,7 +1731,8 @@ private void CopyDatabases(StoreWrapper src, bool recordToAof) if (db.IsDefault()) continue; var dbCopy = new GarnetDatabase(db.MainStore, db.ObjectStore, db.ObjectStoreSizeTracker, - recordToAof ? db.AofDevice : null, recordToAof ? db.AppendOnlyFile : null); + recordToAof ? db.AofDevice : null, recordToAof ? db.AppendOnlyFile : null, + db.MainStoreIndexMaxedOut, db.ObjectStoreIndexMaxedOut); this.TryAddDatabase(dbId, ref dbCopy); } } From 90824ce2bb6ed44df98cfecf72fa0cbf5ba47586 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 18 Feb 2025 17:49:19 -0800 Subject: [PATCH 52/82] wip --- libs/server/Resp/RespServerSession.cs | 1 + libs/server/StoreWrapper.cs | 2 +- test/Garnet.test/MultiDatabaseTests.cs | 86 +++++++++++++++++++++++--- 3 files changed, 80 insertions(+), 9 deletions(-) diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index d412aff0b5..11085d7acf 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -98,6 +98,7 @@ internal sealed unsafe partial class RespServerSession : ServerSessionBase readonly IGarnetAuthenticator _authenticator; internal int activeDbId; + readonly bool allowMultiDb; readonly int maxDbs; ExpandableMap databaseSessions; diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 56b20f9ec2..16995109b5 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -148,7 +148,7 @@ public sealed class StoreWrapper readonly bool allowMultiDb; // Map of databases by database ID (by default: of size 1, contains only DB 0) - ExpandableMap databases; + private readonly ExpandableMap databases; // Array containing active database IDs int[] activeDbIds; diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index eb9b152919..11df0b083e 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -261,7 +261,7 @@ public void MultiDatabaseSwapDatabasesTestLC() ClassicAssert.AreEqual(expectedResponse, actualValue); // Add data to DB 1 - lightClientRequest.SendCommand($"SELECT 1"); + response = lightClientRequest.SendCommand($"SELECT 1"); expectedResponse = "+OK\r\n"; actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); ClassicAssert.AreEqual(expectedResponse, actualValue); @@ -277,7 +277,7 @@ public void MultiDatabaseSwapDatabasesTestLC() ClassicAssert.AreEqual(expectedResponse, actualValue); // Add data to DB 11 - lightClientRequest.SendCommand($"SELECT 11"); + response = lightClientRequest.SendCommand($"SELECT 11"); expectedResponse = "+OK\r\n"; actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); ClassicAssert.AreEqual(expectedResponse, actualValue); @@ -293,7 +293,7 @@ public void MultiDatabaseSwapDatabasesTestLC() ClassicAssert.AreEqual(expectedResponse, actualValue); // Swap DB 1 AND DB 11 (from DB 11 context) - lightClientRequest.SendCommand($"SWAPDB 1 11"); + response = lightClientRequest.SendCommand($"SWAPDB 1 11"); expectedResponse = "+OK\r\n"; actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); ClassicAssert.AreEqual(expectedResponse, actualValue); @@ -310,7 +310,7 @@ public void MultiDatabaseSwapDatabasesTestLC() ClassicAssert.AreEqual(expectedResponse, actualValue); // Verify data in DB 1 is previous data from DB 11 - lightClientRequest.SendCommand($"SELECT 1"); + response = lightClientRequest.SendCommand($"SELECT 1"); expectedResponse = "+OK\r\n"; actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); ClassicAssert.AreEqual(expectedResponse, actualValue); @@ -326,18 +326,18 @@ public void MultiDatabaseSwapDatabasesTestLC() ClassicAssert.AreEqual(expectedResponse, actualValue); // Swap DB 11 AND DB 0 (from DB 1 context) - lightClientRequest.SendCommand($"SELECT 1"); + response = lightClientRequest.SendCommand($"SELECT 1"); expectedResponse = "+OK\r\n"; actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); ClassicAssert.AreEqual(expectedResponse, actualValue); - lightClientRequest.SendCommand($"SWAPDB 11 0"); + response = lightClientRequest.SendCommand($"SWAPDB 11 0"); expectedResponse = "+OK\r\n"; actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); ClassicAssert.AreEqual(expectedResponse, actualValue); // Verify data in DB 0 is previous data from DB 11 - lightClientRequest.SendCommand($"SELECT 0"); + response = lightClientRequest.SendCommand($"SELECT 0"); expectedResponse = "+OK\r\n"; actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); ClassicAssert.AreEqual(expectedResponse, actualValue); @@ -353,7 +353,7 @@ public void MultiDatabaseSwapDatabasesTestLC() ClassicAssert.AreEqual(expectedResponse, actualValue); // Verify data in DB 11 is previous data from DB 0 - lightClientRequest.SendCommand($"SELECT 11"); + response = lightClientRequest.SendCommand($"SELECT 11"); expectedResponse = "+OK\r\n"; actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); ClassicAssert.AreEqual(expectedResponse, actualValue); @@ -369,6 +369,76 @@ public void MultiDatabaseSwapDatabasesTestLC() ClassicAssert.AreEqual(expectedResponse, actualValue); } + [Test] + [Ignore("")] + public void MultiDatabaseMultiSessionSwapDatabasesTestLC() + { + var db1Key1 = "db1:key1"; + var db1Key2 = "db1:key2"; + var db2Key1 = "db2:key1"; + var db2Key2 = "db2:key2"; + var db12Key1 = "db12:key1"; + var db12Key2 = "db12:key2"; + + using var lightClientRequest1 = TestUtils.CreateRequest(); // Session for DB 0 context + using var lightClientRequest2 = TestUtils.CreateRequest(); // Session for DB 1 context + + // Add data to DB 0 + var response = lightClientRequest1.SendCommand($"SET {db1Key1} db1:value1"); + var expectedResponse = "+OK\r\n"; + var actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest1.SendCommand($"LPUSH {db1Key2} db1:val1 db1:val2"); + expectedResponse = ":2\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + // Add data to DB 1 + response = lightClientRequest2.SendCommand($"SELECT 1"); + expectedResponse = "+OK\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest2.SendCommand($"SET {db2Key1} db2:value1"); + expectedResponse = "+OK\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest2.SendCommand($"SADD {db2Key2} db2:val1 db2:val2"); + expectedResponse = ":2\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + // Swap DB 0 AND DB 1 (from DB 0 context) + response = lightClientRequest1.SendCommand($"SWAPDB 0 1"); + expectedResponse = "+OK\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + // Verify data in DB 0 is previous data from DB 1 + response = lightClientRequest1.SendCommand($"GET {db2Key1}", 2); + expectedResponse = "$10\r\ndb2:value1\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest1.SendCommand($"SISMEMBER {db2Key2} db2:val2"); + expectedResponse = ":1\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + // Verify data in DB 1 is previous data from DB 0 + response = lightClientRequest2.SendCommand($"GET {db1Key1}", 2); + expectedResponse = "$10\r\ndb1:value1\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest2.SendCommand($"LPOP {db1Key2}", 2); + expectedResponse = "$8\r\ndb1:val2\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + } + [Test] [Ignore("")] public void MultiDatabaseSelectMultithreadedTestLC() From 2ddec8a5339d1d7b642344616f6f542500628957 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 20 Feb 2025 17:31:50 -0800 Subject: [PATCH 53/82] IDatabaseManager refactor - wip (broken) --- libs/server/Databases/DatabaseManagerBase.cs | 77 +++++ libs/server/Databases/IDatabaseManager.cs | 64 ++++ libs/server/Databases/MultiDatabaseManager.cs | 263 +++++++++++++++++ .../server/Databases/SingleDatabaseManager.cs | 93 ++++++ libs/server/StoreWrapper.cs | 276 +----------------- 5 files changed, 506 insertions(+), 267 deletions(-) create mode 100644 libs/server/Databases/DatabaseManagerBase.cs create mode 100644 libs/server/Databases/IDatabaseManager.cs create mode 100644 libs/server/Databases/MultiDatabaseManager.cs create mode 100644 libs/server/Databases/SingleDatabaseManager.cs diff --git a/libs/server/Databases/DatabaseManagerBase.cs b/libs/server/Databases/DatabaseManagerBase.cs new file mode 100644 index 0000000000..098d3d6906 --- /dev/null +++ b/libs/server/Databases/DatabaseManagerBase.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Diagnostics; +using Microsoft.Extensions.Logging; +using Tsavorite.core; + +namespace Garnet.server +{ + using MainStoreAllocator = SpanByteAllocator>; + using MainStoreFunctions = StoreFunctions; + + using ObjectStoreAllocator = GenericAllocator>>; + using ObjectStoreFunctions = StoreFunctions>; + + internal abstract class DatabaseManagerBase : IDatabaseManager + { + /// + /// Reference to default database (DB 0) + /// + public abstract ref GarnetDatabase DefaultDatabase { get; } + + /// + public TsavoriteKV MainStore => DefaultDatabase.MainStore; + + /// + public TsavoriteKV ObjectStore => DefaultDatabase.ObjectStore; + + /// + public TsavoriteLog AppendOnlyFile => DefaultDatabase.AppendOnlyFile; + + /// + public DateTimeOffset LastSaveTime => DefaultDatabase.LastSaveTime; + + /// + public CacheSizeTracker ObjectStoreSizeTracker => DefaultDatabase.ObjectStoreSizeTracker; + + /// + public WatchVersionMap VersionMap => DefaultDatabase.VersionMap; + + /// + public abstract int DatabaseCount { get; } + + /// + public abstract bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db); + + public abstract void RecoverCheckpoint(bool failOnRecoveryError, bool replicaRecover = false, bool recoverMainStoreFromToken = false, bool recoverObjectStoreFromToken = false, CheckpointMetadata metadata = null); + + public abstract FunctionsState CreateFunctionsState(CustomCommandManager customCommandManager, GarnetObjectSerializer garnetObjectSerializer, int dbId = 0); + + protected bool Disposed; + + protected void RecoverDatabaseCheckpoint(ref GarnetDatabase db) + { + var storeVersion = db.MainStore.Recover(); + long objectStoreVersion = -1; + if (db.ObjectStore != null) + objectStoreVersion = db.ObjectStore.Recover(); + + if (storeVersion > 0 || objectStoreVersion > 0) + { + db.LastSaveTime = DateTimeOffset.UtcNow; + } + } + + private void RecoverDatabaseAOF(ref GarnetDatabase db) + { + if (db.AppendOnlyFile == null) return; + + db.AppendOnlyFile.Recover(); + logger?.LogInformation("Recovered AOF: begin address = {beginAddress}, tail address = {tailAddress}", db.AppendOnlyFile.BeginAddress, db.AppendOnlyFile.TailAddress); + } + + public abstract void Dispose(); + } +} diff --git a/libs/server/Databases/IDatabaseManager.cs b/libs/server/Databases/IDatabaseManager.cs new file mode 100644 index 0000000000..4c0088a52b --- /dev/null +++ b/libs/server/Databases/IDatabaseManager.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using Tsavorite.core; + +namespace Garnet.server +{ + using MainStoreAllocator = SpanByteAllocator>; + using MainStoreFunctions = StoreFunctions; + + using ObjectStoreAllocator = GenericAllocator>>; + using ObjectStoreFunctions = StoreFunctions>; + + internal interface IDatabaseManager : IDisposable + { + /// + /// Store (of DB 0) + /// + public TsavoriteKV MainStore { get; } + + /// + /// Object store (of DB 0) + /// + public TsavoriteKV ObjectStore { get; } + + /// + /// AOF (of DB 0) + /// + public TsavoriteLog AppendOnlyFile { get; } + + /// + /// Last save time (of DB 0) + /// + public DateTimeOffset LastSaveTime { get; } + + /// + /// Object store size tracker (of DB 0) + /// + public CacheSizeTracker ObjectStoreSizeTracker { get; } + + /// + /// Version map (of DB 0) + /// + internal WatchVersionMap VersionMap { get; } + + /// + /// Number of current logical databases + /// + public int DatabaseCount { get; } + + /// + /// Try to get or add a new database + /// + /// Database ID + /// Database + /// True if database was retrieved or added successfully + public bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db); + + public void RecoverCheckpoint(bool failOnRecoveryError, bool replicaRecover = false, bool recoverMainStoreFromToken = false, bool recoverObjectStoreFromToken = false, CheckpointMetadata metadata = null); + + internal FunctionsState CreateFunctionsState(CustomCommandManager customCommandManager, GarnetObjectSerializer garnetObjectSerializer, int dbId = 0); + } +} diff --git a/libs/server/Databases/MultiDatabaseManager.cs b/libs/server/Databases/MultiDatabaseManager.cs new file mode 100644 index 0000000000..c3b3445c92 --- /dev/null +++ b/libs/server/Databases/MultiDatabaseManager.cs @@ -0,0 +1,263 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Garnet.common; +using Microsoft.Extensions.Logging; +using Tsavorite.core; + +namespace Garnet.server +{ + internal class MultiDatabaseManager : DatabaseManagerBase + { + /// + public override ref GarnetDatabase DefaultDatabase => ref databases.Map[0]; + + /// + public override int DatabaseCount => activeDbIdsLength; + + /// + /// Delegate for creating a new logical database + /// + readonly StoreWrapper.DatabaseCreatorDelegate createDatabaseDelegate; + + readonly CancellationTokenSource cts = new(); + + readonly int maxDatabases; + + // Map of databases by database ID (by default: of size 1, contains only DB 0) + ExpandableMap databases; + + SingleWriterMultiReaderLock databasesLock; + + // Array containing active database IDs + int[] activeDbIds; + + // Total number of current active database IDs + int activeDbIdsLength; + + // Last DB ID activated + int lastActivatedDbId = -1; + + // Reusable task array for tracking checkpointing of multiple DBs + // Used by recurring checkpointing task if multiple DBs exist + Task[] checkpointTasks; + + // Reusable task array for tracking aof commits of multiple DBs + // Used by recurring aof commits task if multiple DBs exist + Task[] aofTasks; + readonly object activeDbIdsLock = new(); + + // Path of serialization for the DB IDs file used when committing / recovering to / from AOF + readonly string aofParentDir; + + readonly string aofDirBaseName; + + // Path of serialization for the DB IDs file used when committing / recovering to / from a checkpoint + readonly string checkpointParentDir; + + readonly string checkpointDirBaseName; + + public MultiDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabaseDelegate, int maxDatabases, bool createDefaultDatabase = true) + { + this.createDatabaseDelegate = createsDatabaseDelegate; + this.maxDatabases = maxDatabases; + + // Create default databases map of size 1 + databases = new ExpandableMap(1, 0, this.maxDatabases - 1); + + // Create default database of index 0 (unless specified otherwise) + if (createDefaultDatabase) + { + var db = createDatabaseDelegate(0, out var storeCheckpointDir, out var aofDir); + + var checkpointDirInfo = new DirectoryInfo(storeCheckpointDir); + this.checkpointDirBaseName = checkpointDirInfo.Name; + this.checkpointParentDir = checkpointDirInfo.Parent!.FullName; + + if (aofDir != null) + { + var aofDirInfo = new DirectoryInfo(aofDir); + this.aofDirBaseName = aofDirInfo.Name; + this.aofParentDir = aofDirInfo.Parent!.FullName; + } + + // Set new database in map + if (!this.TryAddDatabase(0, ref db)) + throw new GarnetException("Failed to set initial database in databases map"); + } + } + + public override void RecoverCheckpoint(bool failOnRecoveryError, bool replicaRecover = false, bool recoverMainStoreFromToken = false, bool recoverObjectStoreFromToken = false, CheckpointMetadata metadata = null) + { + if (replicaRecover) + throw new GarnetException( + $"Unexpected call to {nameof(MultiDatabaseManager)}.{nameof(RecoverCheckpoint)} with {nameof(replicaRecover)} == true."); + + try + { + if (!TryGetSavedDatabaseIds(checkpointParentDir, checkpointDirBaseName, out var dbIdsToRecover)) + return; + + foreach (var dbId in dbIdsToRecover) + { + if (!TryGetOrAddDatabase(dbId, out var db)) + throw new GarnetException($"Failed create initial database for recovery (DB ID = {dbId})."); + + RecoverDatabaseCheckpoint(ref db); + } + } + catch (TsavoriteNoHybridLogException ex) + { + // No hybrid log being found is not the same as an error in recovery. e.g. fresh start + logger?.LogInformation(ex, "No Hybrid Log found for recovery; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}", storeVersion, objectStoreVersion); + } + catch (Exception ex) + { + logger?.LogInformation(ex, "Error during recovery of store; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}", storeVersion, objectStoreVersion); + if (failOnRecoveryError) + throw; + } + } + + public override FunctionsState CreateFunctionsState(CustomCommandManager customCommandManager, GarnetObjectSerializer garnetObjectSerializer, int dbId = 0) + { + if (!this.TryGetOrAddDatabase(dbId, out var db)) + throw new GarnetException($"Database with ID {dbId} was not found."); + + return new(db.AppendOnlyFile, db.VersionMap, customCommandManager, null, db.ObjectStoreSizeTracker, + garnetObjectSerializer); + } + + /// + public override bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db) + { + db = default; + + if (!databases.TryGetOrSet(dbId, () => createDatabaseDelegate(dbId, out _, out _), out db, out var added)) + return false; + + if (added) + HandleDatabaseAdded(dbId); + + return true; + } + + /// + /// Try to add a new database + /// + /// Database ID + /// Database + /// + private bool TryAddDatabase(int dbId, ref GarnetDatabase db) + { + if (!databases.TrySetValue(dbId, ref db)) + return false; + + HandleDatabaseAdded(dbId); + return true; + } + + /// + /// Handle a new database added + /// + /// ID of database added + private void HandleDatabaseAdded(int dbId) + { + // If size tracker exists and is stopped, start it (only if DB 0 size tracker is started as well) + var db = databases.Map[dbId]; + if (dbId != 0 && ObjectStoreSizeTracker != null && !ObjectStoreSizeTracker.Stopped && + db.ObjectStoreSizeTracker != null && db.ObjectStoreSizeTracker.Stopped) + db.ObjectStoreSizeTracker.Start(cts.Token); + + var dbIdIdx = Interlocked.Increment(ref lastActivatedDbId); + + // If there is no size increase needed for activeDbIds, set the added ID in the array + if (activeDbIds != null && dbIdIdx < activeDbIds.Length) + { + activeDbIds[dbIdIdx] = dbId; + Interlocked.Increment(ref activeDbIdsLength); + return; + } + + lock (activeDbIdsLock) + { + // Select the next size of activeDbIds (as multiple of 2 from the existing size) + var newSize = activeDbIds?.Length ?? 1; + while (dbIdIdx + 1 > newSize) + { + newSize = Math.Min(maxDatabases, newSize * 2); + } + + // Set an updated instance of activeDbIds + var activeDbIdsSnapshot = activeDbIds; + var activeDbIdsUpdated = new int[newSize]; + + if (activeDbIdsSnapshot != null) + { + Array.Copy(activeDbIdsSnapshot, activeDbIdsUpdated, dbIdIdx); + } + + // Set the last added ID + activeDbIdsUpdated[dbIdIdx] = dbId; + + activeDbIds = activeDbIdsUpdated; + activeDbIdsLength = dbIdIdx + 1; + checkpointTasks = new Task[activeDbIdsLength]; + aofTasks = new Task[activeDbIdsLength]; + } + } + + /// + /// Retrieves saved database IDs from parent checkpoint / AOF path + /// e.g. if path contains directories: baseName, baseName_1, baseName_2, baseName_10 + /// DB IDs 0,1,2,10 will be returned + /// + /// Parent path + /// Base name of directories containing database-specific checkpoints / AOFs + /// DB IDs extracted from parent path + /// True if successful + private bool TryGetSavedDatabaseIds(string path, string baseName, out int[] dbIds) + { + dbIds = default; + if (!Directory.Exists(path)) return false; + + var dirs = Directory.GetDirectories(path, $"{baseName}*", SearchOption.TopDirectoryOnly); + dbIds = new int[dirs.Length]; + for (var i = 0; i < dirs.Length; i++) + { + var dirName = new DirectoryInfo(dirs[i]).Name; + var sepIdx = dirName.IndexOf('_'); + var dbId = 0; + + if (sepIdx != -1 && !int.TryParse(dirName.AsSpan(sepIdx + 1), out dbId)) + continue; + + dbIds[i] = dbId; + } + + return true; + } + + public override void Dispose() + { + if (Disposed) return; + + cts.Cancel(); + + // Disable changes to databases map and dispose all databases + databases.mapLock.WriteLock(); + foreach (var db in databases.Map) + db.Dispose(); + + cts.Dispose(); + + Disposed = true; + } + } +} diff --git a/libs/server/Databases/SingleDatabaseManager.cs b/libs/server/Databases/SingleDatabaseManager.cs new file mode 100644 index 0000000000..926906722f --- /dev/null +++ b/libs/server/Databases/SingleDatabaseManager.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using Microsoft.Extensions.Logging; +using Tsavorite.core; +using static System.Formats.Asn1.AsnWriter; + +namespace Garnet.server +{ + internal class SingleDatabaseManager : DatabaseManagerBase + { + /// + public override ref GarnetDatabase DefaultDatabase => ref defaultDatabase; + + /// + public override int DatabaseCount => 1; + + GarnetDatabase defaultDatabase; + + public SingleDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabaseDelegate) + { + defaultDatabase = createsDatabaseDelegate(0, out _, out _); + } + + /// + public override bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db) + { + ArgumentOutOfRangeException.ThrowIfNotEqual(dbId, 0); + + db = DefaultDatabase; + return true; + } + + public override void RecoverCheckpoint(bool failOnRecoveryError, bool replicaRecover = false, bool recoverMainStoreFromToken = false, bool recoverObjectStoreFromToken = false, CheckpointMetadata metadata = null) + { + long storeVersion = -1, objectStoreVersion = -1; + try + { + if (replicaRecover) + { + // Note: Since replicaRecover only pertains to cluster-mode, we can use the default store pointers (since multi-db mode is disabled in cluster-mode) + if (metadata.storeIndexToken != default && metadata.storeHlogToken != default) + { + storeVersion = !recoverMainStoreFromToken ? MainStore.Recover() : MainStore.Recover(metadata.storeIndexToken, metadata.storeHlogToken); + } + + if (ObjectStore != null) + { + if (metadata.objectStoreIndexToken != default && metadata.objectStoreHlogToken != default) + { + objectStoreVersion = !recoverObjectStoreFromToken ? ObjectStore.Recover() : ObjectStore.Recover(metadata.objectStoreIndexToken, metadata.objectStoreHlogToken); + } + } + + if (storeVersion > 0 || objectStoreVersion > 0) + DefaultDatabase.LastSaveTime = DateTimeOffset.UtcNow; + } + else + { + RecoverDatabaseCheckpoint(ref DefaultDatabase); + } + } + catch (TsavoriteNoHybridLogException ex) + { + // No hybrid log being found is not the same as an error in recovery. e.g. fresh start + logger?.LogInformation(ex, "No Hybrid Log found for recovery; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}", storeVersion, objectStoreVersion); + } + catch (Exception ex) + { + logger?.LogInformation(ex, "Error during recovery of store; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}", storeVersion, objectStoreVersion); + if (failOnRecoveryError) + throw; + } + } + + public override FunctionsState CreateFunctionsState(CustomCommandManager customCommandManager, GarnetObjectSerializer garnetObjectSerializer, int dbId = 0) + { + ArgumentOutOfRangeException.ThrowIfNotEqual(dbId, 0); + + return new(AppendOnlyFile, VersionMap, customCommandManager, null, ObjectStoreSizeTracker, garnetObjectSerializer); + } + + public override void Dispose() + { + if (Disposed) return; + + DefaultDatabase.Dispose(); + + Disposed = true; + } + } +} diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 16995109b5..bf86059056 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -34,40 +34,35 @@ public sealed class StoreWrapper readonly IGarnetServer server; internal readonly long startupTime; - /// - /// Reference to default database (DB 0) - /// - public ref GarnetDatabase DefaultDatabase => ref databases.Map[0]; - /// /// Store (of DB 0) /// - public TsavoriteKV store => DefaultDatabase.MainStore; + public TsavoriteKV store => databaseManager.MainStore; /// /// Object store (of DB 0) /// - public TsavoriteKV objectStore => DefaultDatabase.ObjectStore; + public TsavoriteKV objectStore => databaseManager.ObjectStore; /// /// AOF (of DB 0) /// - public TsavoriteLog appendOnlyFile => DefaultDatabase.AppendOnlyFile; + public TsavoriteLog appendOnlyFile => databaseManager.AppendOnlyFile; /// /// Last save time (of DB 0) /// - public DateTimeOffset lastSaveTime => DefaultDatabase.LastSaveTime; + public DateTimeOffset lastSaveTime => databaseManager.LastSaveTime; /// /// Object store size tracker (of DB 0) /// - public CacheSizeTracker objectStoreSizeTracker => DefaultDatabase.ObjectStoreSizeTracker; + public CacheSizeTracker objectStoreSizeTracker => databaseManager.ObjectStoreSizeTracker; /// /// Version map (of DB 0) /// - internal WatchVersionMap versionMap => DefaultDatabase.VersionMap; + internal WatchVersionMap versionMap => databaseManager.VersionMap; /// /// Server options @@ -114,20 +109,12 @@ public sealed class StoreWrapper /// public readonly TimeSpan loggingFrequency; - /// - /// Number of current logical databases - /// - public int DatabaseCount => activeDbIdsLength; - /// /// Definition for delegate creating a new logical database /// public delegate GarnetDatabase DatabaseCreatorDelegate(int dbId, out string storeCheckpointDir, out string aofDir); - /// - /// Delegate for creating a new logical database - /// - internal readonly DatabaseCreatorDelegate createDatabasesDelegate; + internal readonly IDatabaseManager databaseManager; internal readonly CollectionItemBroker itemBroker; internal readonly CustomCommandManager customCommandManager; @@ -142,42 +129,10 @@ public sealed class StoreWrapper readonly CancellationTokenSource ctsCommit; SingleWriterMultiReaderLock checkpointTaskLock; - SingleWriterMultiReaderLock databasesLock; // True if this server supports more than one logical database readonly bool allowMultiDb; - // Map of databases by database ID (by default: of size 1, contains only DB 0) - private readonly ExpandableMap databases; - - // Array containing active database IDs - int[] activeDbIds; - - // Total number of current active database IDs - int activeDbIdsLength; - - // Last DB ID activated - int lastActivatedDbId = -1; - - // Reusable task array for tracking checkpointing of multiple DBs - // Used by recurring checkpointing task if multiple DBs exist - Task[] checkpointTasks; - - // Reusable task array for tracking aof commits of multiple DBs - // Used by recurring aof commits task if multiple DBs exist - Task[] aofTasks; - readonly object activeDbIdsLock = new(); - - // Path of serialization for the DB IDs file used when committing / recovering to / from AOF - readonly string aofParentDir; - - readonly string aofDirBaseName; - - // Path of serialization for the DB IDs file used when committing / recovering to / from a checkpoint - readonly string checkpointParentDir; - - readonly string checkpointDirBaseName; - // True if StoreWrapper instance is disposed bool disposed = false; @@ -219,30 +174,6 @@ public StoreWrapper( // If cluster mode is off and more than one database allowed multi-db mode is turned on this.allowMultiDb = !this.serverOptions.EnableCluster && this.serverOptions.MaxDatabases > 1; - // Create default databases map of size 1 - databases = new ExpandableMap(1, 0, this.serverOptions.MaxDatabases - 1); - - // Create default database of index 0 (unless specified otherwise) - if (createDefaultDatabase) - { - var db = createDatabasesDelegate(0, out var storeCheckpointDir, out var aofDir); - - var checkpointDirInfo = new DirectoryInfo(storeCheckpointDir); - this.checkpointDirBaseName = checkpointDirInfo.Name; - this.checkpointParentDir = checkpointDirInfo.Parent!.FullName; - - if (aofDir != null) - { - var aofDirInfo = new DirectoryInfo(aofDir); - this.aofDirBaseName = aofDirInfo.Name; - this.aofParentDir = aofDirInfo.Parent!.FullName; - } - - // Set new database in map - if (!this.TryAddDatabase(0, ref db)) - throw new GarnetException("Failed to set initial database in databases map"); - } - if (serverOptions.SlowLogThreshold > 0) this.slowLogContainer = new SlowLogContainer(serverOptions.SlowLogMaxEntries); @@ -341,20 +272,6 @@ public string GetIp() return localEndpoint.Address.ToString(); } - internal FunctionsState CreateFunctionsState(int dbId = 0) - { - Debug.Assert(dbId == 0 || allowMultiDb); - - if (dbId == 0) - return new(appendOnlyFile, versionMap, customCommandManager, null, objectStoreSizeTracker, GarnetObjectSerializer); - - if (!this.TryGetOrAddDatabase(dbId, out var db)) - throw new GarnetException($"Database with ID {dbId} was not found."); - - return new(db.AppendOnlyFile, db.VersionMap, customCommandManager, null, db.ObjectStoreSizeTracker, - GarnetObjectSerializer); - } - internal void Recover() { if (serverOptions.EnableCluster) @@ -400,22 +317,11 @@ public void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStore } if (storeVersion > 0 || objectStoreVersion > 0) - DefaultDatabase.LastSaveTime = DateTimeOffset.UtcNow; + databaseManager.UpdateDatabaseLastSaveTime(); } else { - if (!allowMultiDb) - { - RecoverDatabaseCheckpoint(0); - } - else - { - if (!TryGetSavedDatabaseIds(checkpointParentDir, checkpointDirBaseName, out var dbIdsToRecover)) - return; - - foreach (var dbId in dbIdsToRecover) - RecoverDatabaseCheckpoint(dbId); - } + databaseManager.RecoverCheckpoint(); } } catch (TsavoriteNoHybridLogException ex) @@ -431,28 +337,6 @@ public void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStore } } - private void RecoverDatabaseCheckpoint(int dbId) - { - long storeVersion = -1, objectStoreVersion = -1; - - var success = TryGetOrAddDatabase(dbId, out var db); - Debug.Assert(success); - - storeVersion = db.MainStore.Recover(); - if (db.ObjectStore != null) objectStoreVersion = db.ObjectStore.Recover(); - - if (storeVersion > 0 || objectStoreVersion > 0) - { - var lastSave = DateTimeOffset.UtcNow; - db.LastSaveTime = lastSave; - if (dbId == 0) - DefaultDatabase.LastSaveTime = lastSave; - - var databasesMapSnapshot = databases.Map; - databasesMapSnapshot[dbId] = db; - } - } - /// /// Recover AOF /// @@ -472,17 +356,6 @@ public void RecoverAOF() } } - private void RecoverDatabaseAOF(int dbId) - { - var success = TryGetOrAddDatabase(dbId, out var db); - Debug.Assert(success); - - if (db.AppendOnlyFile == null) return; - - db.AppendOnlyFile.Recover(); - logger?.LogInformation("Recovered AOF: begin address = {beginAddress}, tail address = {tailAddress}", db.AppendOnlyFile.BeginAddress, db.AppendOnlyFile.TailAddress); - } - /// /// Reset /// @@ -556,40 +429,6 @@ public long ReplayAOF(long untilAddress = -1) return replicationOffset; } - /// - /// Try to add a new database - /// - /// Database ID - /// Database - /// - public bool TryAddDatabase(int dbId, ref GarnetDatabase db) - { - if ((!allowMultiDb && dbId != 0) || !databases.TrySetValue(dbId, ref db)) - return false; - - HandleDatabaseAdded(dbId); - return true; - } - - /// - /// Try to get or add a new database - /// - /// Database ID - /// Database - /// True if database was retrieved or added successfully - public bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db) - { - db = default; - - if ((!allowMultiDb && dbId != 0) || !databases.TryGetOrSet(dbId, () => createDatabasesDelegate(dbId, out _, out _), out db, out var added)) - return false; - - if (added) - HandleDatabaseAdded(dbId); - - return true; - } - /// /// Try to swap between two database instances /// @@ -1293,14 +1132,6 @@ public void Dispose() monitor?.Dispose(); ctsCommit?.Cancel(); - while (objectStoreSizeTracker != null && !objectStoreSizeTracker.Stopped) - Thread.Yield(); - - // Disable changes to databases map and dispose all databases - databases.mapLock.WriteLock(); - foreach (var db in databases.Map) - db.Dispose(); - ctsCommit?.Dispose(); clusterProvider?.Dispose(); } @@ -1736,94 +1567,5 @@ private void CopyDatabases(StoreWrapper src, bool recordToAof) this.TryAddDatabase(dbId, ref dbCopy); } } - - /// - /// Retrieves saved database IDs from parent checkpoint / AOF path - /// e.g. if path contains directories: baseName, baseName_1, baseName_2, baseName_10 - /// DB IDs 0,1,2,10 will be returned - /// - /// Parent path - /// Base name of directories containing database-specific checkpoints / AOFs - /// DB IDs extracted from parent path - /// True if successful - private bool TryGetSavedDatabaseIds(string path, string baseName, out int[] dbIds) - { - dbIds = default; - if (!Directory.Exists(path)) return false; - - try - { - var dirs = Directory.GetDirectories(path, $"{baseName}*", SearchOption.TopDirectoryOnly); - dbIds = new int[dirs.Length]; - for (var i = 0; i < dirs.Length; i++) - { - var dirName = new DirectoryInfo(dirs[i]).Name; - var sepIdx = dirName.IndexOf('_'); - var dbId = 0; - - if (sepIdx != -1 && !int.TryParse(dirName.AsSpan(sepIdx + 1), out dbId)) - continue; - - dbIds[i] = dbId; - } - - return true; - } - catch (Exception e) - { - logger?.LogError(e, "Encountered an error while trying to parse save databases IDs."); - return false; - } - } - - /// - /// Handle a new database added - /// - /// ID of database added - private void HandleDatabaseAdded(int dbId) - { - // If size tracker exists and is stopped, start it (only if DB 0 size tracker is started as well) - var db = databases.Map[dbId]; - if (dbId != 0 && objectStoreSizeTracker != null && !objectStoreSizeTracker.Stopped && - db.ObjectStoreSizeTracker != null && db.ObjectStoreSizeTracker.Stopped) - db.ObjectStoreSizeTracker.Start(ctsCommit.Token); - - var dbIdIdx = Interlocked.Increment(ref lastActivatedDbId); - - // If there is no size increase needed for activeDbIds, set the added ID in the array - if (activeDbIds != null && dbIdIdx < activeDbIds.Length) - { - activeDbIds[dbIdIdx] = dbId; - Interlocked.Increment(ref activeDbIdsLength); - return; - } - - lock (activeDbIdsLock) - { - // Select the next size of activeDbIds (as multiple of 2 from the existing size) - var newSize = activeDbIds?.Length ?? 1; - while (dbIdIdx + 1 > newSize) - { - newSize = Math.Min(this.serverOptions.MaxDatabases, newSize * 2); - } - - // Set an updated instance of activeDbIds - var activeDbIdsSnapshot = activeDbIds; - var activeDbIdsUpdated = new int[newSize]; - - if (activeDbIdsSnapshot != null) - { - Array.Copy(activeDbIdsSnapshot, activeDbIdsUpdated, dbIdIdx); - } - - // Set the last added ID - activeDbIdsUpdated[dbIdIdx] = dbId; - - activeDbIds = activeDbIdsUpdated; - activeDbIdsLength = dbIdIdx + 1; - checkpointTasks = new Task[activeDbIdsLength]; - aofTasks = new Task[activeDbIdsLength]; - } - } } } \ No newline at end of file From 3869062fb77a3f5586d91ce2d41483de06012ed3 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 20 Feb 2025 22:10:09 -0800 Subject: [PATCH 54/82] wip (broken) --- libs/host/GarnetServer.cs | 2 +- libs/server/Databases/DatabaseManagerBase.cs | 92 +++++++- libs/server/Databases/IDatabaseManager.cs | 52 ++++- libs/server/Databases/MultiDatabaseManager.cs | 168 ++++++++++++-- .../server/Databases/SingleDatabaseManager.cs | 85 ++++++- libs/server/GarnetDatabase.cs | 30 ++- libs/server/StoreWrapper.cs | 210 +----------------- 7 files changed, 398 insertions(+), 241 deletions(-) diff --git a/libs/host/GarnetServer.cs b/libs/host/GarnetServer.cs index 93e6af035c..b417f784c2 100644 --- a/libs/host/GarnetServer.cs +++ b/libs/host/GarnetServer.cs @@ -279,7 +279,7 @@ private GarnetDatabase CreateDatabase(int dbId, GarnetServerOptions serverOption var objectStore = CreateObjectStore(dbId, clusterFactory, customCommandManager, checkpointDir, out var objectStoreSizeTracker); var (aofDevice, aof) = CreateAOF(dbId, out aofDir); - return new GarnetDatabase(store, objectStore, objectStoreSizeTracker, aofDevice, aof, + return new GarnetDatabase(dbId, store, objectStore, objectStoreSizeTracker, aofDevice, aof, serverOptions.AdjustedIndexMaxCacheLines == 0, serverOptions.AdjustedObjectStoreIndexMaxCacheLines == 0); } diff --git a/libs/server/Databases/DatabaseManagerBase.cs b/libs/server/Databases/DatabaseManagerBase.cs index 098d3d6906..469a227769 100644 --- a/libs/server/Databases/DatabaseManagerBase.cs +++ b/libs/server/Databases/DatabaseManagerBase.cs @@ -16,9 +16,7 @@ namespace Garnet.server internal abstract class DatabaseManagerBase : IDatabaseManager { - /// - /// Reference to default database (DB 0) - /// + /// public abstract ref GarnetDatabase DefaultDatabase { get; } /// @@ -42,19 +40,56 @@ internal abstract class DatabaseManagerBase : IDatabaseManager /// public abstract int DatabaseCount { get; } + /// + /// The main logger instance associated with the database manager. + /// + protected ILogger Logger; + + /// + /// Store Wrapper + /// + public readonly StoreWrapper StoreWrapper; + /// public abstract bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db); - public abstract void RecoverCheckpoint(bool failOnRecoveryError, bool replicaRecover = false, bool recoverMainStoreFromToken = false, bool recoverObjectStoreFromToken = false, CheckpointMetadata metadata = null); + public abstract void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStoreFromToken = false, bool recoverObjectStoreFromToken = false, CheckpointMetadata metadata = null); + + /// + public abstract void RecoverAOF(); + + /// + public abstract long ReplayAOF(long untilAddress = -1); + + /// + public abstract void Reset(int dbId = 0); + + /// + public abstract void EnqueueCommit(bool isMainStore, long version, int dbId = 0); + + /// + public abstract bool TryGetDatabase(int dbId, out GarnetDatabase db); + + /// + public abstract void FlushDatabase(bool unsafeTruncateLog, int dbId = 0); + + /// + public abstract void FlushAllDatabases(bool unsafeTruncateLog); public abstract FunctionsState CreateFunctionsState(CustomCommandManager customCommandManager, GarnetObjectSerializer garnetObjectSerializer, int dbId = 0); protected bool Disposed; - protected void RecoverDatabaseCheckpoint(ref GarnetDatabase db) + protected DatabaseManagerBase(StoreWrapper storeWrapper) + { + this.StoreWrapper = storeWrapper; + } + + protected void RecoverDatabaseCheckpoint(ref GarnetDatabase db, out long storeVersion, out long objectStoreVersion) { - var storeVersion = db.MainStore.Recover(); - long objectStoreVersion = -1; + storeVersion = db.MainStore.Recover(); + objectStoreVersion = -1; + if (db.ObjectStore != null) objectStoreVersion = db.ObjectStore.Recover(); @@ -64,12 +99,51 @@ protected void RecoverDatabaseCheckpoint(ref GarnetDatabase db) } } - private void RecoverDatabaseAOF(ref GarnetDatabase db) + protected void RecoverDatabaseAOF(ref GarnetDatabase db) { if (db.AppendOnlyFile == null) return; db.AppendOnlyFile.Recover(); - logger?.LogInformation("Recovered AOF: begin address = {beginAddress}, tail address = {tailAddress}", db.AppendOnlyFile.BeginAddress, db.AppendOnlyFile.TailAddress); + Logger?.LogInformation($"Recovered AOF: begin address = {db.AppendOnlyFile.BeginAddress}, tail address = {db.AppendOnlyFile.TailAddress}"); + } + + protected void ResetDatabase(ref GarnetDatabase db) + { + try + { + if (db.MainStore.Log.TailAddress > 64) + db.MainStore.Reset(); + if (db.ObjectStore?.Log.TailAddress > 64) + db.ObjectStore?.Reset(); + db.AppendOnlyFile?.Reset(); + + var lastSave = DateTimeOffset.FromUnixTimeSeconds(0); + db.LastSaveTime = lastSave; + } + catch (Exception ex) + { + Logger?.LogError(ex, "Error during reset of store"); + } + } + + protected void EnqueueDatabaseCommit(ref GarnetDatabase db, bool isMainStore, long version) + { + if (db.AppendOnlyFile == null) return; + + AofHeader header = new() + { + opType = isMainStore ? AofEntryType.MainStoreCheckpointCommit : AofEntryType.ObjectStoreCheckpointCommit, + storeVersion = version, + sessionID = -1 + }; + + db.AppendOnlyFile.Enqueue(header, out _); + } + + protected void FlushDatabase(ref GarnetDatabase db, bool unsafeTruncateLog) + { + db.MainStore.Log.ShiftBeginAddress(db.MainStore.Log.TailAddress, truncateLog: unsafeTruncateLog); + db.ObjectStore?.Log.ShiftBeginAddress(db.ObjectStore.Log.TailAddress, truncateLog: unsafeTruncateLog); } public abstract void Dispose(); diff --git a/libs/server/Databases/IDatabaseManager.cs b/libs/server/Databases/IDatabaseManager.cs index 4c0088a52b..4fb61ffcc7 100644 --- a/libs/server/Databases/IDatabaseManager.cs +++ b/libs/server/Databases/IDatabaseManager.cs @@ -14,6 +14,11 @@ namespace Garnet.server internal interface IDatabaseManager : IDisposable { + /// + /// Reference to default database (DB 0) + /// + public ref GarnetDatabase DefaultDatabase { get; } + /// /// Store (of DB 0) /// @@ -57,7 +62,52 @@ internal interface IDatabaseManager : IDisposable /// True if database was retrieved or added successfully public bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db); - public void RecoverCheckpoint(bool failOnRecoveryError, bool replicaRecover = false, bool recoverMainStoreFromToken = false, bool recoverObjectStoreFromToken = false, CheckpointMetadata metadata = null); + public void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStoreFromToken = false, bool recoverObjectStoreFromToken = false, CheckpointMetadata metadata = null); + + /// + /// Recover AOF + /// + public void RecoverAOF(); + + /// + /// When replaying AOF we do not want to write AOF records again. + /// + public long ReplayAOF(long untilAddress = -1); + + /// + /// Reset + /// + /// Database ID + public void Reset(int dbId = 0); + + /// + /// Append a checkpoint commit to the AOF + /// + /// + /// + /// + public void EnqueueCommit(bool isMainStore, long version, int dbId = 0); + + /// + /// Get database DB ID + /// + /// DB Id + /// Database + /// True if database found + public bool TryGetDatabase(int dbId, out GarnetDatabase db); + + /// + /// Flush database with specified ID + /// + /// Truncate log + /// Database ID + public void FlushDatabase(bool unsafeTruncateLog, int dbId = 0); + + /// + /// Flush all active databases + /// + /// Truncate log + public void FlushAllDatabases(bool unsafeTruncateLog); internal FunctionsState CreateFunctionsState(CustomCommandManager customCommandManager, GarnetObjectSerializer garnetObjectSerializer, int dbId = 0); } diff --git a/libs/server/Databases/MultiDatabaseManager.cs b/libs/server/Databases/MultiDatabaseManager.cs index c3b3445c92..725100e956 100644 --- a/libs/server/Databases/MultiDatabaseManager.cs +++ b/libs/server/Databases/MultiDatabaseManager.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics; -using System.Globalization; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -63,10 +62,12 @@ internal class MultiDatabaseManager : DatabaseManagerBase readonly string checkpointDirBaseName; - public MultiDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabaseDelegate, int maxDatabases, bool createDefaultDatabase = true) + public MultiDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabaseDelegate, StoreWrapper storeWrapper, int maxDatabases, + ILoggerFactory loggerFactory = null, bool createDefaultDatabase = true) : base(storeWrapper) { this.createDatabaseDelegate = createsDatabaseDelegate; this.maxDatabases = maxDatabases; + this.Logger = loggerFactory?.CreateLogger(nameof(MultiDatabaseManager)); // Create default databases map of size 1 databases = new ExpandableMap(1, 0, this.maxDatabases - 1); @@ -93,38 +94,95 @@ public MultiDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabase } } - public override void RecoverCheckpoint(bool failOnRecoveryError, bool replicaRecover = false, bool recoverMainStoreFromToken = false, bool recoverObjectStoreFromToken = false, CheckpointMetadata metadata = null) + public override void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStoreFromToken = false, bool recoverObjectStoreFromToken = false, CheckpointMetadata metadata = null) { if (replicaRecover) throw new GarnetException( $"Unexpected call to {nameof(MultiDatabaseManager)}.{nameof(RecoverCheckpoint)} with {nameof(replicaRecover)} == true."); + int[] dbIdsToRecover; try { - if (!TryGetSavedDatabaseIds(checkpointParentDir, checkpointDirBaseName, out var dbIdsToRecover)) + if (!TryGetSavedDatabaseIds(checkpointParentDir, checkpointDirBaseName, out dbIdsToRecover)) return; - foreach (var dbId in dbIdsToRecover) - { - if (!TryGetOrAddDatabase(dbId, out var db)) - throw new GarnetException($"Failed create initial database for recovery (DB ID = {dbId})."); + } + catch (Exception ex) + { + Logger?.LogInformation(ex, $"Error during recovery of database ids; checkpointParentDir = {checkpointParentDir}; checkpointDirBaseName = {checkpointDirBaseName}"); + if (StoreWrapper.serverOptions.FailOnRecoveryError) + throw; + return; + } - RecoverDatabaseCheckpoint(ref db); + long storeVersion = -1, objectStoreVersion = -1; + + foreach (var dbId in dbIdsToRecover) + { + if (!TryGetOrAddDatabase(dbId, out var db)) + throw new GarnetException($"Failed to retrieve or create database for checkpoint recovery (DB ID = {dbId})."); + + try + { + RecoverDatabaseCheckpoint(ref db, out storeVersion, out objectStoreVersion); + } + catch (TsavoriteNoHybridLogException ex) + { + // No hybrid log being found is not the same as an error in recovery. e.g. fresh start + Logger?.LogInformation(ex, $"No Hybrid Log found for recovery; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}", storeVersion, objectStoreVersion); + } + catch (Exception ex) + { + Logger?.LogInformation(ex, $"Error during recovery of store; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}", storeVersion, objectStoreVersion); + if (StoreWrapper.serverOptions.FailOnRecoveryError) + throw; } } - catch (TsavoriteNoHybridLogException ex) + } + + /// + public override void RecoverAOF() + { + int[] dbIdsToRecover; + try { - // No hybrid log being found is not the same as an error in recovery. e.g. fresh start - logger?.LogInformation(ex, "No Hybrid Log found for recovery; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}", storeVersion, objectStoreVersion); + if (!TryGetSavedDatabaseIds(aofParentDir, aofDirBaseName, out dbIdsToRecover)) + return; + } catch (Exception ex) { - logger?.LogInformation(ex, "Error during recovery of store; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}", storeVersion, objectStoreVersion); - if (failOnRecoveryError) - throw; + Logger?.LogInformation(ex, $"Error during recovery of database ids; aofParentDir = {aofParentDir}; aofDirBaseName = {aofDirBaseName}"); + return; + } + + foreach (var dbId in dbIdsToRecover) + { + if (!TryGetOrAddDatabase(dbId, out var db)) + throw new GarnetException($"Failed to retrieve or create database for AOF recovery (DB ID = {dbId})."); + + RecoverDatabaseAOF(ref db); } } + public override long ReplayAOF(long untilAddress = -1) => throw new NotImplementedException(); + + public override void Reset(int dbId = 0) + { + if (!this.TryGetOrAddDatabase(dbId, out var db)) + throw new GarnetException($"Database with ID {dbId} was not found."); + + ResetDatabase(ref db); + } + + public override void EnqueueCommit(bool isMainStore, long version, int dbId = 0) + { + if (!this.TryGetOrAddDatabase(dbId, out var db)) + throw new GarnetException($"Database with ID {dbId} was not found."); + + EnqueueDatabaseCommit(ref db, isMainStore, version); + } + public override FunctionsState CreateFunctionsState(CustomCommandManager customCommandManager, GarnetObjectSerializer garnetObjectSerializer, int dbId = 0) { if (!this.TryGetOrAddDatabase(dbId, out var db)) @@ -137,8 +195,6 @@ public override FunctionsState CreateFunctionsState(CustomCommandManager customC /// public override bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db) { - db = default; - if (!databases.TryGetOrSet(dbId, () => createDatabaseDelegate(dbId, out _, out _), out db, out var added)) return false; @@ -148,6 +204,53 @@ public override bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db) return true; } + /// + public override bool TryGetDatabase(int dbId, out GarnetDatabase db) + { + var databasesMapSize = databases.ActualSize; + var databasesMapSnapshot = databases.Map; + + if (dbId == 0) + { + db = databasesMapSnapshot[0]; + Debug.Assert(!db.IsDefault()); + return true; + } + + // Check if database already exists + if (dbId < databasesMapSize) + { + db = databasesMapSnapshot[dbId]; + if (!db.IsDefault()) return true; + } + + // Try to retrieve or add database + return this.TryGetOrAddDatabase(dbId, out db); + } + + /// + public override void FlushDatabase(bool unsafeTruncateLog, int dbId = 0) + { + if (!this.TryGetOrAddDatabase(dbId, out var db)) + throw new GarnetException($"Database with ID {dbId} was not found."); + + FlushDatabase(ref db, unsafeTruncateLog); + } + + /// + public override void FlushAllDatabases(bool unsafeTruncateLog) + { + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + var databaseMapSnapshot = databases.Map; + + for (var i = 0; i < activeDbIdsSize; i++) + { + var dbId = activeDbIdsSnapshot[i]; + this.FlushDatabase(ref databaseMapSnapshot[dbId], unsafeTruncateLog); + } + } + /// /// Try to add a new database /// @@ -244,6 +347,37 @@ private bool TryGetSavedDatabaseIds(string path, string baseName, out int[] dbId return true; } + /// + /// Copy active databases from specified IDatabaseManager instance + /// + /// Source IDatabaseManager + /// True if should enable AOF in copied databases + private void CopyDatabases(IDatabaseManager src, bool enableAof) + { + switch (src) + { + case SingleDatabaseManager sdbm: + var defaultDbCopy = new GarnetDatabase(ref sdbm.DefaultDatabase, enableAof); + this.TryAddDatabase(0, ref defaultDbCopy); + return; + case MultiDatabaseManager mdbm: + var activeDbIdsSize = mdbm.activeDbIdsLength; + var activeDbIdsSnapshot = mdbm.activeDbIds; + var databasesMapSnapshot = mdbm.databases.Map; + + for (var i = 0; i < activeDbIdsSize; i++) + { + var dbId = activeDbIdsSnapshot[i]; + var dbCopy = new GarnetDatabase(ref databasesMapSnapshot[dbId], enableAof); + this.TryAddDatabase(dbId, ref dbCopy); + } + + return; + default: + throw new NotImplementedException(); + } + } + public override void Dispose() { if (Disposed) return; diff --git a/libs/server/Databases/SingleDatabaseManager.cs b/libs/server/Databases/SingleDatabaseManager.cs index 926906722f..2a8357c691 100644 --- a/libs/server/Databases/SingleDatabaseManager.cs +++ b/libs/server/Databases/SingleDatabaseManager.cs @@ -4,7 +4,6 @@ using System; using Microsoft.Extensions.Logging; using Tsavorite.core; -using static System.Formats.Asn1.AsnWriter; namespace Garnet.server { @@ -18,8 +17,10 @@ internal class SingleDatabaseManager : DatabaseManagerBase GarnetDatabase defaultDatabase; - public SingleDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabaseDelegate) + public SingleDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabaseDelegate, StoreWrapper storeWrapper, ILoggerFactory loggerFactory = null) : + base(storeWrapper) { + this.Logger = loggerFactory?.CreateLogger(nameof(SingleDatabaseManager)); defaultDatabase = createsDatabaseDelegate(0, out _, out _); } @@ -32,7 +33,7 @@ public override bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db) return true; } - public override void RecoverCheckpoint(bool failOnRecoveryError, bool replicaRecover = false, bool recoverMainStoreFromToken = false, bool recoverObjectStoreFromToken = false, CheckpointMetadata metadata = null) + public override void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStoreFromToken = false, bool recoverObjectStoreFromToken = false, CheckpointMetadata metadata = null) { long storeVersion = -1, objectStoreVersion = -1; try @@ -40,7 +41,7 @@ public override void RecoverCheckpoint(bool failOnRecoveryError, bool replicaRec if (replicaRecover) { // Note: Since replicaRecover only pertains to cluster-mode, we can use the default store pointers (since multi-db mode is disabled in cluster-mode) - if (metadata.storeIndexToken != default && metadata.storeHlogToken != default) + if (metadata!.storeIndexToken != default && metadata.storeHlogToken != default) { storeVersion = !recoverMainStoreFromToken ? MainStore.Recover() : MainStore.Recover(metadata.storeIndexToken, metadata.storeHlogToken); } @@ -58,22 +59,90 @@ public override void RecoverCheckpoint(bool failOnRecoveryError, bool replicaRec } else { - RecoverDatabaseCheckpoint(ref DefaultDatabase); + RecoverDatabaseCheckpoint(ref DefaultDatabase, out storeVersion, out objectStoreVersion); } } catch (TsavoriteNoHybridLogException ex) { // No hybrid log being found is not the same as an error in recovery. e.g. fresh start - logger?.LogInformation(ex, "No Hybrid Log found for recovery; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}", storeVersion, objectStoreVersion); + Logger?.LogInformation(ex, $"No Hybrid Log found for recovery; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}"); } catch (Exception ex) { - logger?.LogInformation(ex, "Error during recovery of store; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}", storeVersion, objectStoreVersion); - if (failOnRecoveryError) + Logger?.LogInformation(ex, $"Error during recovery of store; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}"); + + if (StoreWrapper.serverOptions.FailOnRecoveryError) throw; } } + /// + public override void RecoverAOF() => RecoverDatabaseAOF(ref DefaultDatabase); + + /// + public override long ReplayAOF(long untilAddress = -1) + { + if (!StoreWrapper.serverOptions.EnableAOF) + return -1; + + long replicationOffset = 0; + try + { + // When replaying AOF we do not want to write record again to AOF. + // So initialize local AofProcessor with recordToAof: false. + var aofProcessor = new AofProcessor(StoreWrapper, recordToAof: false, Logger); + + aofProcessor.Recover(0, untilAddress); + DefaultDatabase.LastSaveTime = DateTimeOffset.UtcNow; + replicationOffset = aofProcessor.ReplicationOffset; + + aofProcessor.Dispose(); + } + catch (Exception ex) + { + Logger?.LogError(ex, "Error during recovery of AofProcessor"); + if (StoreWrapper.serverOptions.FailOnRecoveryError) + throw; + } + + return replicationOffset; + } + + /// + public override void Reset(int dbId = 0) + { + ArgumentOutOfRangeException.ThrowIfNotEqual(dbId, 0); + + ResetDatabase(ref DefaultDatabase); + } + + /// + public override void EnqueueCommit(bool isMainStore, long version, int dbId = 0) + { + ArgumentOutOfRangeException.ThrowIfNotEqual(dbId, 0); + + EnqueueDatabaseCommit(ref DefaultDatabase, isMainStore, version); + } + + /// + public override bool TryGetDatabase(int dbId, out GarnetDatabase db) + { + ArgumentOutOfRangeException.ThrowIfNotEqual(dbId, 0); + + db = DefaultDatabase; + return true; + } + + public override void FlushDatabase(bool unsafeTruncateLog, int dbId = 0) + { + ArgumentOutOfRangeException.ThrowIfNotEqual(dbId, 0); + + FlushDatabase(ref DefaultDatabase, unsafeTruncateLog); + } + + public override void FlushAllDatabases(bool unsafeTruncateLog) => + FlushDatabase(ref DefaultDatabase, unsafeTruncateLog); + public override FunctionsState CreateFunctionsState(CustomCommandManager customCommandManager, GarnetObjectSerializer garnetObjectSerializer, int dbId = 0) { ArgumentOutOfRangeException.ThrowIfNotEqual(dbId, 0); diff --git a/libs/server/GarnetDatabase.cs b/libs/server/GarnetDatabase.cs index d77b7c84f8..797f8f6ecc 100644 --- a/libs/server/GarnetDatabase.cs +++ b/libs/server/GarnetDatabase.cs @@ -21,6 +21,11 @@ public struct GarnetDatabase : IDisposable // TODO: Change map size to a reasonable number const int DefaultVersionMapSize = 1 << 16; + /// + /// Database ID + /// + public int Id; + /// /// Main Store /// @@ -72,22 +77,39 @@ public struct GarnetDatabase : IDisposable bool disposed = false; - public GarnetDatabase(TsavoriteKV mainStore, + public GarnetDatabase(int id, TsavoriteKV mainStore, TsavoriteKV objectStore, CacheSizeTracker objectStoreSizeTracker, IDevice aofDevice, TsavoriteLog appendOnlyFile, - bool mainStoreIndexMaxedOut, bool objectStoreIndexMaxedOut) + bool mainStoreIndexMaxedOut, bool objectStoreIndexMaxedOut) : this() { + Id = id; MainStore = mainStore; ObjectStore = objectStore; ObjectStoreSizeTracker = objectStoreSizeTracker; AofDevice = aofDevice; AppendOnlyFile = appendOnlyFile; + MainStoreIndexMaxedOut = mainStoreIndexMaxedOut; + ObjectStoreIndexMaxedOut = objectStoreIndexMaxedOut; + } + + public GarnetDatabase(ref GarnetDatabase srcDb, bool enableAof) : this() + { + Id = srcDb.Id; + MainStore = srcDb.MainStore; + ObjectStore = srcDb.ObjectStore; + ObjectStoreSizeTracker = srcDb.ObjectStoreSizeTracker; + AofDevice = enableAof ? AofDevice : null; + AppendOnlyFile = enableAof ? AppendOnlyFile : null; + MainStoreIndexMaxedOut = srcDb.MainStoreIndexMaxedOut; + ObjectStoreIndexMaxedOut = srcDb.ObjectStoreIndexMaxedOut; + } + + public GarnetDatabase() + { VersionMap = new WatchVersionMap(DefaultVersionMapSize); LastSaveStoreTailAddress = 0; LastSaveObjectStoreTailAddress = 0; LastSaveTime = DateTimeOffset.FromUnixTimeSeconds(0); - MainStoreIndexMaxedOut = mainStoreIndexMaxedOut; - ObjectStoreIndexMaxedOut = objectStoreIndexMaxedOut; } public bool IsDefault() => MainStore == null; diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index bf86059056..2835a1213f 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -5,7 +5,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Net; using System.Net.Sockets; using System.Threading; @@ -285,102 +284,13 @@ internal void Recover() { if (serverOptions.Recover) { - RecoverCheckpoint(); - RecoverAOF(); + databaseManager.RecoverCheckpoint(); + databaseManager.RecoverAOF(); ReplayAOF(); } } } - /// - /// Caller will have to decide if recover is necessary, so we do not check if recover option is enabled - /// - public void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStoreFromToken = false, bool recoverObjectStoreFromToken = false, CheckpointMetadata metadata = null) - { - long storeVersion = -1, objectStoreVersion = -1; - try - { - if (replicaRecover) - { - // Note: Since replicaRecover only pertains to cluster-mode, we can use the default store pointers (since multi-db mode is disabled in cluster-mode) - if (metadata.storeIndexToken != default && metadata.storeHlogToken != default) - { - storeVersion = !recoverMainStoreFromToken ? store.Recover() : store.Recover(metadata.storeIndexToken, metadata.storeHlogToken); - } - - if (!serverOptions.DisableObjects) - { - if (metadata.objectStoreIndexToken != default && metadata.objectStoreHlogToken != default) - { - objectStoreVersion = !recoverObjectStoreFromToken ? objectStore.Recover() : objectStore.Recover(metadata.objectStoreIndexToken, metadata.objectStoreHlogToken); - } - } - - if (storeVersion > 0 || objectStoreVersion > 0) - databaseManager.UpdateDatabaseLastSaveTime(); - } - else - { - databaseManager.RecoverCheckpoint(); - } - } - catch (TsavoriteNoHybridLogException ex) - { - // No hybrid log being found is not the same as an error in recovery. e.g. fresh start - logger?.LogInformation(ex, "No Hybrid Log found for recovery; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}", storeVersion, objectStoreVersion); - } - catch (Exception ex) - { - logger?.LogInformation(ex, "Error during recovery of store; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}", storeVersion, objectStoreVersion); - if (serverOptions.FailOnRecoveryError) - throw; - } - } - - /// - /// Recover AOF - /// - public void RecoverAOF() - { - if (!allowMultiDb) - { - RecoverDatabaseAOF(0); - } - else if (aofParentDir != null) - { - if (!TryGetSavedDatabaseIds(aofParentDir, aofDirBaseName, out var dbIdsToRecover)) - return; - - foreach (var dbId in dbIdsToRecover) - RecoverDatabaseAOF(dbId); - } - } - - /// - /// Reset - /// - public void Reset(int dbId = 0) - { - try - { - ref var db = ref databases.Map[dbId]; - if (db.MainStore.Log.TailAddress > 64) - db.MainStore.Reset(); - if (db.ObjectStore?.Log.TailAddress > 64) - db.ObjectStore?.Reset(); - db.AppendOnlyFile?.Reset(); - - var lastSave = DateTimeOffset.FromUnixTimeSeconds(0); - if (dbId == 0) - DefaultDatabase.LastSaveTime = lastSave; - db.LastSaveTime = lastSave; - } - catch (Exception ex) - { - logger?.LogError(ex, "Error during reset of store"); - } - } - /// /// When replaying AOF we do not want to write AOF records again. /// @@ -701,25 +611,6 @@ void DoCompaction(ref GarnetDatabase db) DoCompaction(ref db, serverOptions.CompactionMaxSegments, serverOptions.ObjectStoreCompactionMaxSegments, 1, serverOptions.CompactionType, serverOptions.CompactionForceDelete); } - /// - /// Append a checkpoint commit to the AOF - /// - /// - /// - /// - public void EnqueueCommit(bool isMainStore, long version, int dbId = 0) - { - AofHeader header = new() - { - opType = isMainStore ? AofEntryType.MainStoreCheckpointCommit : AofEntryType.ObjectStoreCheckpointCommit, - storeVersion = version, - sessionID = -1 - }; - - var aof = databases.Map[dbId].AppendOnlyFile; - aof?.Enqueue(header, out _); - } - void DoCompaction(ref GarnetDatabase db, int mainStoreMaxSegments, int objectStoreMaxSegments, int numSegmentsToCompact, LogCompactionType compactionType, bool compactionForceDelete) { if (compactionType == LogCompactionType.None) return; @@ -1381,9 +1272,9 @@ private async Task CheckpointDatabaseTask(int dbId, StoreType storeType, ILogger databasesMapSnapshot[dbId] = db; } - private async Task InitiateCheckpoint(int dbId, GarnetDatabase db, bool full, CheckpointType checkpointType, bool tryIncremental, StoreType storeType, ILogger logger = null) + private async Task InitiateCheckpoint(GarnetDatabase db, bool full, CheckpointType checkpointType, bool tryIncremental, StoreType storeType, ILogger logger = null) { - logger?.LogInformation("Initiating checkpoint; full = {full}, type = {checkpointType}, tryIncremental = {tryIncremental}, storeType = {storeType}, dbId = {dbId}", full, checkpointType, tryIncremental, storeType, dbId); + logger?.LogInformation("Initiating checkpoint; full = {full}, type = {checkpointType}, tryIncremental = {tryIncremental}, storeType = {storeType}, dbId = {dbId}", full, checkpointType, tryIncremental, storeType, db.Id); long CheckpointCoveredAofAddress = 0; if (db.AppendOnlyFile != null) @@ -1444,16 +1335,13 @@ private async Task InitiateCheckpoint(int dbId, GarnetDatabase db, bool full, Ch logger?.LogInformation("Completed checkpoint"); } - public bool HasKeysInSlots(List slots, int dbId = 0) + public bool HasKeysInSlots(List slots) { if (slots.Count > 0) { - var dbFound = TryGetDatabase(dbId, out var db); - Debug.Assert(dbFound); - bool hasKeyInSlots = false; { - using var iter = db.MainStore.Iterate>(new SimpleSessionFunctions()); + using var iter = store.Iterate>(new SimpleSessionFunctions()); while (!hasKeyInSlots && iter.GetNext(out RecordInfo record)) { ref var key = ref iter.GetKey(); @@ -1465,11 +1353,11 @@ public bool HasKeysInSlots(List slots, int dbId = 0) } } - if (!hasKeyInSlots && db.ObjectStore != null) + if (!hasKeyInSlots && objectStore != null) { - var functionsState = CreateFunctionsState(); + var functionsState = databaseManager.CreateFunctionsState(customCommandManager, GarnetObjectSerializer); var objstorefunctions = new ObjectSessionFunctions(functionsState); - var objectStoreSession = db.ObjectStore?.NewSession(objstorefunctions); + var objectStoreSession = objectStore?.NewSession(objstorefunctions); var iter = objectStoreSession.Iterate(); while (!hasKeyInSlots && iter.GetNext(out RecordInfo record)) { @@ -1487,85 +1375,5 @@ public bool HasKeysInSlots(List slots, int dbId = 0) return false; } - - /// - /// Get database DB ID - /// - /// DB Id - /// Database - /// True if database found - public bool TryGetDatabase(int dbId, out GarnetDatabase db) - { - var databasesMapSize = databases.ActualSize; - var databasesMapSnapshot = databases.Map; - - if (dbId == 0) - { - db = databasesMapSnapshot[0]; - Debug.Assert(!db.IsDefault()); - return true; - } - - // Check if database already exists - if (dbId < databasesMapSize) - { - db = databasesMapSnapshot[dbId]; - if (!db.IsDefault()) return true; - } - - // Try to retrieve or add database - return this.TryGetOrAddDatabase(dbId, out db); - } - - /// - /// Flush database with specified ID - /// - /// Truncate log - /// Database ID - public void FlushDatabase(bool unsafeTruncateLog, int dbId = 0) - { - var dbFound = TryGetDatabase(dbId, out var db); - Debug.Assert(dbFound); - - db.MainStore.Log.ShiftBeginAddress(db.MainStore.Log.TailAddress, truncateLog: unsafeTruncateLog); - db.ObjectStore?.Log.ShiftBeginAddress(db.ObjectStore.Log.TailAddress, truncateLog: unsafeTruncateLog); - } - - /// - /// Flush all active databases - /// - /// Truncate log - public void FlushAllDatabases(bool unsafeTruncateLog) - { - var activeDbIdsSize = activeDbIdsLength; - var activeDbIdsSnapshot = activeDbIds; - for (var i = 0; i < activeDbIdsSize; i++) - { - var dbId = activeDbIdsSnapshot[i]; - this.FlushDatabase(unsafeTruncateLog, dbId); - } - } - - /// - /// Copy active databases from specified StoreWrapper instance - /// - /// Source StoreWrapper - /// True if should enable AOF in copied databases - private void CopyDatabases(StoreWrapper src, bool recordToAof) - { - var databasesMapSize = src.databases.ActualSize; - var databasesMapSnapshot = src.databases.Map; - - for (var dbId = 0; dbId < databasesMapSize; dbId++) - { - var db = databasesMapSnapshot[dbId]; - if (db.IsDefault()) continue; - - var dbCopy = new GarnetDatabase(db.MainStore, db.ObjectStore, db.ObjectStoreSizeTracker, - recordToAof ? db.AofDevice : null, recordToAof ? db.AppendOnlyFile : null, - db.MainStoreIndexMaxedOut, db.ObjectStoreIndexMaxedOut); - this.TryAddDatabase(dbId, ref dbCopy); - } - } } } \ No newline at end of file From 0a20f87cc24114c9fe4f939e2dc609ab8ea88fab Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Mon, 24 Feb 2025 15:37:07 -0800 Subject: [PATCH 55/82] wip --- libs/server/AOF/AofProcessor.cs | 21 ++++--- libs/server/Databases/DatabaseManagerBase.cs | 45 ++++++++++++-- libs/server/Databases/IDatabaseManager.cs | 9 ++- libs/server/Databases/MultiDatabaseManager.cs | 54 +++++++++++++---- .../server/Databases/SingleDatabaseManager.cs | 46 +++++++++------ libs/server/Resp/RespServerSession.cs | 4 +- libs/server/StoreWrapper.cs | 59 +++---------------- 7 files changed, 138 insertions(+), 100 deletions(-) diff --git a/libs/server/AOF/AofProcessor.cs b/libs/server/AOF/AofProcessor.cs index 854bdce352..04e671792c 100644 --- a/libs/server/AOF/AofProcessor.cs +++ b/libs/server/AOF/AofProcessor.cs @@ -72,7 +72,7 @@ public AofProcessor( this.activeDbId = 0; this.respServerSession = new RespServerSession(0, networkSender: null, storeWrapper: replayAofStoreWrapper, subscribeBroker: null, authenticator: null, enableScripts: false); - SwitchActiveDatabaseContext(0, true); + SwitchActiveDatabaseContext(ref storeWrapper.DefaultDatabase, true); parseState.Initialize(); storeInput.parseState = parseState; @@ -99,23 +99,22 @@ public void Dispose() /// /// Recover store using AOF /// - public unsafe void Recover(int dbId = 0, long untilAddress = -1) + public unsafe void Recover(ref GarnetDatabase db, long untilAddress = -1) { logger?.LogInformation("Begin AOF recovery"); - RecoverReplay(dbId, untilAddress); + RecoverReplay(ref db, untilAddress); } MemoryResult output = default; - private unsafe void RecoverReplay(int dbId, long untilAddress) + private unsafe void RecoverReplay(ref GarnetDatabase db, long untilAddress) { logger?.LogInformation("Begin AOF replay"); try { int count = 0; - storeWrapper.TryGetOrAddDatabase(dbId, out var db); var appendOnlyFile = db.AppendOnlyFile; - SwitchActiveDatabaseContext(dbId); + SwitchActiveDatabaseContext(ref db); if (untilAddress == -1) untilAddress = appendOnlyFile.TailAddress; using var scan = appendOnlyFile.Scan(appendOnlyFile.BeginAddress, untilAddress); @@ -266,22 +265,22 @@ private unsafe bool ReplayOp(byte* entryPtr) return true; } - private void SwitchActiveDatabaseContext(int dbId, bool initialSetup = false) + private void SwitchActiveDatabaseContext(ref GarnetDatabase db, bool initialSetup = false) { - if (respServerSession.activeDbId != dbId) + if (respServerSession.activeDbId != db.Id) { - var switchDbSuccessful = respServerSession.TrySwitchActiveDatabaseSession(dbId); + var switchDbSuccessful = respServerSession.TrySwitchActiveDatabaseSession(db.Id); Debug.Assert(switchDbSuccessful); } - if (this.activeDbId != dbId || initialSetup) + if (this.activeDbId != db.Id || initialSetup) { var session = respServerSession.storageSession.basicContext.Session; basicContext = session.BasicContext; var objectStoreSession = respServerSession.storageSession.objectStoreBasicContext.Session; if (objectStoreSession is not null) objectStoreBasicContext = objectStoreSession.BasicContext; - this.activeDbId = dbId; + this.activeDbId = db.Id; } } diff --git a/libs/server/Databases/DatabaseManagerBase.cs b/libs/server/Databases/DatabaseManagerBase.cs index 469a227769..3b5a1a24a6 100644 --- a/libs/server/Databases/DatabaseManagerBase.cs +++ b/libs/server/Databases/DatabaseManagerBase.cs @@ -40,11 +40,6 @@ internal abstract class DatabaseManagerBase : IDatabaseManager /// public abstract int DatabaseCount { get; } - /// - /// The main logger instance associated with the database manager. - /// - protected ILogger Logger; - /// /// Store Wrapper /// @@ -78,11 +73,28 @@ internal abstract class DatabaseManagerBase : IDatabaseManager public abstract FunctionsState CreateFunctionsState(CustomCommandManager customCommandManager, GarnetObjectSerializer garnetObjectSerializer, int dbId = 0); + /// + /// Delegate for creating a new logical database + /// + protected readonly StoreWrapper.DatabaseCreatorDelegate CreateDatabaseDelegate; + + /// + /// The main logger instance associated with the database manager. + /// + protected ILogger Logger; + + /// + /// The logger factory used to create logger instances + /// + protected ILoggerFactory LoggerFactory; + protected bool Disposed; - protected DatabaseManagerBase(StoreWrapper storeWrapper) + protected DatabaseManagerBase(StoreWrapper.DatabaseCreatorDelegate createDatabaseDelegate, StoreWrapper storeWrapper, ILoggerFactory loggerFactory = null) { + this.CreateDatabaseDelegate = createDatabaseDelegate; this.StoreWrapper = storeWrapper; + this.LoggerFactory = loggerFactory; } protected void RecoverDatabaseCheckpoint(ref GarnetDatabase db, out long storeVersion, out long objectStoreVersion) @@ -107,6 +119,25 @@ protected void RecoverDatabaseAOF(ref GarnetDatabase db) Logger?.LogInformation($"Recovered AOF: begin address = {db.AppendOnlyFile.BeginAddress}, tail address = {db.AppendOnlyFile.TailAddress}"); } + protected long ReplayDatabaseAOF(AofProcessor aofProcessor, ref GarnetDatabase db, long untilAddress = -1) + { + long replicationOffset = 0; + try + { + aofProcessor.Recover(ref db, untilAddress); + db.LastSaveTime = DateTimeOffset.UtcNow; + replicationOffset = aofProcessor.ReplicationOffset; + } + catch (Exception ex) + { + Logger?.LogError(ex, "Error during recovery of AofProcessor"); + if (StoreWrapper.serverOptions.FailOnRecoveryError) + throw; + } + + return replicationOffset; + } + protected void ResetDatabase(ref GarnetDatabase db) { try @@ -147,5 +178,7 @@ protected void FlushDatabase(ref GarnetDatabase db, bool unsafeTruncateLog) } public abstract void Dispose(); + + public abstract IDatabaseManager Clone(bool enableAof); } } diff --git a/libs/server/Databases/IDatabaseManager.cs b/libs/server/Databases/IDatabaseManager.cs index 4fb61ffcc7..b2edebe642 100644 --- a/libs/server/Databases/IDatabaseManager.cs +++ b/libs/server/Databases/IDatabaseManager.cs @@ -12,7 +12,7 @@ namespace Garnet.server using ObjectStoreAllocator = GenericAllocator>>; using ObjectStoreFunctions = StoreFunctions>; - internal interface IDatabaseManager : IDisposable + public interface IDatabaseManager : IDisposable { /// /// Reference to default database (DB 0) @@ -109,6 +109,13 @@ internal interface IDatabaseManager : IDisposable /// Truncate log public void FlushAllDatabases(bool unsafeTruncateLog); + /// + /// Create a shallow copy of the IDatabaseManager instance and copy databases to the new instance + /// + /// True if AOF should be enabled in the clone + /// + public IDatabaseManager Clone(bool enableAof); + internal FunctionsState CreateFunctionsState(CustomCommandManager customCommandManager, GarnetObjectSerializer garnetObjectSerializer, int dbId = 0); } } diff --git a/libs/server/Databases/MultiDatabaseManager.cs b/libs/server/Databases/MultiDatabaseManager.cs index 725100e956..9c8163b676 100644 --- a/libs/server/Databases/MultiDatabaseManager.cs +++ b/libs/server/Databases/MultiDatabaseManager.cs @@ -20,11 +20,6 @@ internal class MultiDatabaseManager : DatabaseManagerBase /// public override int DatabaseCount => activeDbIdsLength; - /// - /// Delegate for creating a new logical database - /// - readonly StoreWrapper.DatabaseCreatorDelegate createDatabaseDelegate; - readonly CancellationTokenSource cts = new(); readonly int maxDatabases; @@ -63,9 +58,8 @@ internal class MultiDatabaseManager : DatabaseManagerBase readonly string checkpointDirBaseName; public MultiDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabaseDelegate, StoreWrapper storeWrapper, int maxDatabases, - ILoggerFactory loggerFactory = null, bool createDefaultDatabase = true) : base(storeWrapper) + ILoggerFactory loggerFactory = null, bool createDefaultDatabase = true) : base(createsDatabaseDelegate, storeWrapper, loggerFactory) { - this.createDatabaseDelegate = createsDatabaseDelegate; this.maxDatabases = maxDatabases; this.Logger = loggerFactory?.CreateLogger(nameof(MultiDatabaseManager)); @@ -75,7 +69,7 @@ public MultiDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabase // Create default database of index 0 (unless specified otherwise) if (createDefaultDatabase) { - var db = createDatabaseDelegate(0, out var storeCheckpointDir, out var aofDir); + var db = CreateDatabaseDelegate(0, out var storeCheckpointDir, out var aofDir); var checkpointDirInfo = new DirectoryInfo(storeCheckpointDir); this.checkpointDirBaseName = checkpointDirInfo.Name; @@ -94,6 +88,12 @@ public MultiDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabase } } + public MultiDatabaseManager(MultiDatabaseManager src, bool enableAof) : this(src.CreateDatabaseDelegate, + src.StoreWrapper, src.maxDatabases, src.LoggerFactory, false) + { + this.CopyDatabases(src, enableAof); + } + public override void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStoreFromToken = false, bool recoverObjectStoreFromToken = false, CheckpointMetadata metadata = null) { if (replicaRecover) @@ -165,7 +165,37 @@ public override void RecoverAOF() } } - public override long ReplayAOF(long untilAddress = -1) => throw new NotImplementedException(); + public override long ReplayAOF(long untilAddress = -1) + { + if (!StoreWrapper.serverOptions.EnableAOF) + return -1; + + // When replaying AOF we do not want to write record again to AOF. + // So initialize local AofProcessor with recordToAof: false. + var aofProcessor = new AofProcessor(StoreWrapper, recordToAof: false, Logger); + + long replicationOffset = 0; + try + { + var databasesMapSnapshot = databases.Map; + + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + + for (var i = 0; i < activeDbIdsSize; i++) + { + var dbId = activeDbIdsSnapshot[i]; + var offset = ReplayDatabaseAOF(aofProcessor, ref databasesMapSnapshot[dbId], dbId == 0 ? untilAddress : -1); + if (dbId == 0) replicationOffset = offset; + } + } + finally + { + aofProcessor.Dispose(); + } + + return replicationOffset; + } public override void Reset(int dbId = 0) { @@ -183,6 +213,8 @@ public override void EnqueueCommit(bool isMainStore, long version, int dbId = 0) EnqueueDatabaseCommit(ref db, isMainStore, version); } + public override IDatabaseManager Clone(bool enableAof) => new MultiDatabaseManager(this, enableAof); + public override FunctionsState CreateFunctionsState(CustomCommandManager customCommandManager, GarnetObjectSerializer garnetObjectSerializer, int dbId = 0) { if (!this.TryGetOrAddDatabase(dbId, out var db)) @@ -195,7 +227,7 @@ public override FunctionsState CreateFunctionsState(CustomCommandManager customC /// public override bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db) { - if (!databases.TryGetOrSet(dbId, () => createDatabaseDelegate(dbId, out _, out _), out db, out var added)) + if (!databases.TryGetOrSet(dbId, () => CreateDatabaseDelegate(dbId, out _, out _), out db, out var added)) return false; if (added) @@ -352,7 +384,7 @@ private bool TryGetSavedDatabaseIds(string path, string baseName, out int[] dbId /// /// Source IDatabaseManager /// True if should enable AOF in copied databases - private void CopyDatabases(IDatabaseManager src, bool enableAof) + protected void CopyDatabases(IDatabaseManager src, bool enableAof) { switch (src) { diff --git a/libs/server/Databases/SingleDatabaseManager.cs b/libs/server/Databases/SingleDatabaseManager.cs index 2a8357c691..3c9c5baebf 100644 --- a/libs/server/Databases/SingleDatabaseManager.cs +++ b/libs/server/Databases/SingleDatabaseManager.cs @@ -17,11 +17,21 @@ internal class SingleDatabaseManager : DatabaseManagerBase GarnetDatabase defaultDatabase; - public SingleDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabaseDelegate, StoreWrapper storeWrapper, ILoggerFactory loggerFactory = null) : - base(storeWrapper) + public SingleDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabaseDelegate, StoreWrapper storeWrapper, ILoggerFactory loggerFactory = null, bool createDefaultDatabase = true) : + base(createsDatabaseDelegate, storeWrapper) { this.Logger = loggerFactory?.CreateLogger(nameof(SingleDatabaseManager)); - defaultDatabase = createsDatabaseDelegate(0, out _, out _); + + // Create default database of index 0 (unless specified otherwise) + if (createDefaultDatabase) + { + defaultDatabase = createsDatabaseDelegate(0, out _, out _); + } + } + + public SingleDatabaseManager(SingleDatabaseManager src, bool enableAof) : this(src.CreateDatabaseDelegate, src.StoreWrapper, src.LoggerFactory) + { + this.CopyDatabases(src, enableAof); } /// @@ -85,27 +95,18 @@ public override long ReplayAOF(long untilAddress = -1) if (!StoreWrapper.serverOptions.EnableAOF) return -1; - long replicationOffset = 0; + // When replaying AOF we do not want to write record again to AOF. + // So initialize local AofProcessor with recordToAof: false. + var aofProcessor = new AofProcessor(StoreWrapper, recordToAof: false, Logger); + try { - // When replaying AOF we do not want to write record again to AOF. - // So initialize local AofProcessor with recordToAof: false. - var aofProcessor = new AofProcessor(StoreWrapper, recordToAof: false, Logger); - - aofProcessor.Recover(0, untilAddress); - DefaultDatabase.LastSaveTime = DateTimeOffset.UtcNow; - replicationOffset = aofProcessor.ReplicationOffset; - - aofProcessor.Dispose(); + return ReplayDatabaseAOF(aofProcessor, ref DefaultDatabase, untilAddress); } - catch (Exception ex) + finally { - Logger?.LogError(ex, "Error during recovery of AofProcessor"); - if (StoreWrapper.serverOptions.FailOnRecoveryError) - throw; + aofProcessor.Dispose(); } - - return replicationOffset; } /// @@ -143,6 +144,8 @@ public override void FlushDatabase(bool unsafeTruncateLog, int dbId = 0) public override void FlushAllDatabases(bool unsafeTruncateLog) => FlushDatabase(ref DefaultDatabase, unsafeTruncateLog); + public override IDatabaseManager Clone(bool enableAof) => new SingleDatabaseManager(this, enableAof); + public override FunctionsState CreateFunctionsState(CustomCommandManager customCommandManager, GarnetObjectSerializer garnetObjectSerializer, int dbId = 0) { ArgumentOutOfRangeException.ThrowIfNotEqual(dbId, 0); @@ -150,6 +153,11 @@ public override FunctionsState CreateFunctionsState(CustomCommandManager customC return new(AppendOnlyFile, VersionMap, customCommandManager, null, ObjectStoreSizeTracker, garnetObjectSerializer); } + private void CopyDatabases(SingleDatabaseManager src, bool enableAof) + { + this.DefaultDatabase = new GarnetDatabase(ref src.DefaultDatabase, enableAof); + } + public override void Dispose() { if (Disposed) return; diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index 11085d7acf..9da87a2e27 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -1269,10 +1269,10 @@ internal bool TrySwitchActiveDatabaseSession(int dbId) { if (!allowMultiDb) return false; - if (!databaseSessions.TryGetOrSet(dbId, () => CreateDatabaseSession(dbId), out var db, out _)) + if (!databaseSessions.TryGetOrSet(dbId, () => CreateDatabaseSession(dbId), out var dbSession, out _)) return false; - SwitchActiveDatabaseSession(dbId, ref db); + SwitchActiveDatabaseSession(dbId, ref dbSession); return true; } diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 2835a1213f..f3db734a92 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -33,6 +33,11 @@ public sealed class StoreWrapper readonly IGarnetServer server; internal readonly long startupTime; + /// + /// Default database (DB 0) + /// + public ref GarnetDatabase DefaultDatabase => ref databaseManager.DefaultDatabase; + /// /// Store (of DB 0) /// @@ -142,11 +147,10 @@ public StoreWrapper( string version, string redisProtocolVersion, IGarnetServer server, - DatabaseCreatorDelegate createsDatabaseDelegate, + IDatabaseManager databaseManager, CustomCommandManager customCommandManager, GarnetServerOptions serverOptions, SubscribeBroker subscribeBroker, - bool createDefaultDatabase = true, AccessControlList accessControlList = null, IClusterFactory clusterFactory = null, ILoggerFactory loggerFactory = null) @@ -158,7 +162,7 @@ public StoreWrapper( this.serverOptions = serverOptions; this.subscribeBroker = subscribeBroker; this.customCommandManager = customCommandManager; - this.createDatabasesDelegate = createsDatabaseDelegate; + this.databaseManager = databaseManager; this.monitor = serverOptions.MetricsSamplingFrequency > 0 ? new GarnetServerMonitor(this, serverOptions, server, loggerFactory?.CreateLogger("GarnetServerMonitor")) @@ -229,16 +233,14 @@ public StoreWrapper(StoreWrapper storeWrapper, bool recordToAof) : this( storeWrapper.version, storeWrapper.redisProtocolVersion, storeWrapper.server, - storeWrapper.createDatabasesDelegate, + storeWrapper.databaseManager.Clone(recordToAof), storeWrapper.customCommandManager, storeWrapper.serverOptions, storeWrapper.subscribeBroker, - createDefaultDatabase: false, storeWrapper.accessControlList, null, storeWrapper.loggerFactory) { - this.CopyDatabases(storeWrapper, recordToAof); } /// @@ -294,50 +296,7 @@ internal void Recover() /// /// When replaying AOF we do not want to write AOF records again. /// - public long ReplayAOF(long untilAddress = -1) - { - if (!serverOptions.EnableAOF) - return -1; - - long replicationOffset = 0; - try - { - // When replaying AOF we do not want to write record again to AOF. - // So initialize local AofProcessor with recordToAof: false. - var aofProcessor = new AofProcessor(this, recordToAof: false, logger); - - var databasesMapSnapshot = databases.Map; - - var activeDbIdsSize = activeDbIdsLength; - var activeDbIdsSnapshot = activeDbIds; - - for (var i = 0; i < activeDbIdsSize; i++) - { - var dbId = activeDbIdsSnapshot[i]; - - aofProcessor.Recover(dbId, dbId == 0 ? untilAddress : -1); - - var lastSave = DateTimeOffset.UtcNow; - databasesMapSnapshot[dbId].LastSaveTime = lastSave; - - if (dbId == 0) - { - replicationOffset = aofProcessor.ReplicationOffset; - DefaultDatabase.LastSaveTime = lastSave; - } - } - - aofProcessor.Dispose(); - } - catch (Exception ex) - { - logger?.LogError(ex, "Error during recovery of AofProcessor"); - if (serverOptions.FailOnRecoveryError) - throw; - } - - return replicationOffset; - } + public long ReplayAOF(long untilAddress = -1) => this.databaseManager.ReplayAOF(); /// /// Try to swap between two database instances From d7f5dd770ee4ee7d756ba8b6a607a8576fded9c5 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 25 Feb 2025 17:38:52 -0800 Subject: [PATCH 56/82] wip --- libs/server/AOF/AofProcessor.cs | 4 +- libs/server/Databases/DatabaseManagerBase.cs | 288 ++++++++- libs/server/Databases/IDatabaseManager.cs | 66 ++- libs/server/Databases/MultiDatabaseManager.cs | 272 ++++++++- .../server/Databases/SingleDatabaseManager.cs | 139 ++++- libs/server/GarnetDatabase.cs | 17 +- libs/server/Resp/AdminCommands.cs | 6 +- libs/server/Resp/BasicCommands.cs | 4 +- libs/server/Servers/StoreApi.cs | 10 +- libs/server/Storage/Session/StorageSession.cs | 4 +- libs/server/StoreWrapper.cs | 546 ++---------------- 11 files changed, 814 insertions(+), 542 deletions(-) diff --git a/libs/server/AOF/AofProcessor.cs b/libs/server/AOF/AofProcessor.cs index 04e671792c..09c25bbb96 100644 --- a/libs/server/AOF/AofProcessor.cs +++ b/libs/server/AOF/AofProcessor.cs @@ -199,14 +199,14 @@ public unsafe void ProcessAofRecordInternal(byte* ptr, int length, bool asReplic if (asReplica) { if (header.storeVersion > storeWrapper.store.CurrentVersion) - storeWrapper.TakeCheckpoint(false, StoreType.Main, logger: logger); + storeWrapper.databaseManager.TakeCheckpoint(false, StoreType.Main, logger: logger); } break; case AofEntryType.ObjectStoreCheckpointCommit: if (asReplica) { if (header.storeVersion > storeWrapper.objectStore.CurrentVersion) - storeWrapper.TakeCheckpoint(false, StoreType.Object, logger: logger); + storeWrapper.databaseManager.TakeCheckpoint(false, StoreType.Object, logger: logger); } break; default: diff --git a/libs/server/Databases/DatabaseManagerBase.cs b/libs/server/Databases/DatabaseManagerBase.cs index 3b5a1a24a6..fbd8a246b0 100644 --- a/libs/server/Databases/DatabaseManagerBase.cs +++ b/libs/server/Databases/DatabaseManagerBase.cs @@ -2,7 +2,8 @@ // Licensed under the MIT license. using System; -using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Tsavorite.core; @@ -48,7 +49,37 @@ internal abstract class DatabaseManagerBase : IDatabaseManager /// public abstract bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db); - public abstract void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStoreFromToken = false, bool recoverObjectStoreFromToken = false, CheckpointMetadata metadata = null); + /// + public abstract bool TryPauseCheckpoints(int dbId); + + /// + /// Continuously try to take a lock for checkpointing until acquired or token was cancelled + /// + /// ID of database to lock + /// Cancellation token + /// True if lock acquired + public abstract Task TryPauseCheckpointsContinuousAsync(int dbId, CancellationToken token = default); + + /// + public abstract void ResumeCheckpoints(int dbId); + + /// + public abstract void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStoreFromToken = false, + bool recoverObjectStoreFromToken = false, CheckpointMetadata metadata = null); + + /// + public abstract bool TakeCheckpoint(bool background, StoreType storeType = StoreType.All, ILogger logger = null, + CancellationToken token = default); + + /// + public abstract bool TakeCheckpoint(bool background, int dbId, StoreType storeType = StoreType.All, ILogger logger = null, + CancellationToken token = default); + + /// + public abstract Task TakeOnDemandCheckpointAsync(DateTimeOffset entryTime, int dbId = 0); + + public abstract Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLimit, + CancellationToken token = default, ILogger logger = null); /// public abstract void RecoverAOF(); @@ -71,7 +102,16 @@ internal abstract class DatabaseManagerBase : IDatabaseManager /// public abstract void FlushAllDatabases(bool unsafeTruncateLog); - public abstract FunctionsState CreateFunctionsState(CustomCommandManager customCommandManager, GarnetObjectSerializer garnetObjectSerializer, int dbId = 0); + /// + public abstract bool TrySwapDatabases(int dbId1, int dbId2); + + public abstract FunctionsState CreateFunctionsState(int dbId = 0); + + /// + public abstract void Dispose(); + + /// + public abstract IDatabaseManager Clone(bool enableAof); /// /// Delegate for creating a new logical database @@ -90,11 +130,11 @@ internal abstract class DatabaseManagerBase : IDatabaseManager protected bool Disposed; - protected DatabaseManagerBase(StoreWrapper.DatabaseCreatorDelegate createDatabaseDelegate, StoreWrapper storeWrapper, ILoggerFactory loggerFactory = null) + protected DatabaseManagerBase(StoreWrapper.DatabaseCreatorDelegate createDatabaseDelegate, StoreWrapper storeWrapper) { this.CreateDatabaseDelegate = createDatabaseDelegate; this.StoreWrapper = storeWrapper; - this.LoggerFactory = loggerFactory; + this.LoggerFactory = storeWrapper.loggerFactory; } protected void RecoverDatabaseCheckpoint(ref GarnetDatabase db, out long storeVersion, out long objectStoreVersion) @@ -111,6 +151,123 @@ protected void RecoverDatabaseCheckpoint(ref GarnetDatabase db, out long storeVe } } + protected async Task InitiateCheckpointAsync(GarnetDatabase db, bool full, CheckpointType checkpointType, + bool tryIncremental, + StoreType storeType, ILogger logger = null) + { + logger?.LogInformation("Initiating checkpoint; full = {full}, type = {checkpointType}, tryIncremental = {tryIncremental}, storeType = {storeType}, dbId = {dbId}", full, checkpointType, tryIncremental, storeType, db.Id); + + long checkpointCoveredAofAddress = 0; + if (db.AppendOnlyFile != null) + { + if (StoreWrapper.serverOptions.EnableCluster) + StoreWrapper.clusterProvider.OnCheckpointInitiated(out checkpointCoveredAofAddress); + else + checkpointCoveredAofAddress = db.AppendOnlyFile.TailAddress; + + if (checkpointCoveredAofAddress > 0) + logger?.LogInformation("Will truncate AOF to {tailAddress} after checkpoint (files deleted after next commit)", checkpointCoveredAofAddress); + } + + (bool success, Guid token) storeCheckpointResult = default; + (bool success, Guid token) objectStoreCheckpointResult = default; + if (full) + { + if (storeType is StoreType.Main or StoreType.All) + storeCheckpointResult = await db.MainStore.TakeFullCheckpointAsync(checkpointType); + + if (db.ObjectStore != null && (storeType == StoreType.Object || storeType == StoreType.All)) + objectStoreCheckpointResult = await db.ObjectStore.TakeFullCheckpointAsync(checkpointType); + } + else + { + if (storeType is StoreType.Main or StoreType.All) + storeCheckpointResult = await db.MainStore.TakeHybridLogCheckpointAsync(checkpointType, tryIncremental); + + if (db.ObjectStore != null && (storeType == StoreType.Object || storeType == StoreType.All)) + objectStoreCheckpointResult = await db.ObjectStore.TakeHybridLogCheckpointAsync(checkpointType, tryIncremental); + } + + // If cluster is enabled the replication manager is responsible for truncating AOF + if (StoreWrapper.serverOptions.EnableCluster && StoreWrapper.serverOptions.EnableAOF) + { + StoreWrapper.clusterProvider.SafeTruncateAOF(storeType, full, checkpointCoveredAofAddress, + storeCheckpointResult.token, objectStoreCheckpointResult.token); + } + else + { + db.AppendOnlyFile?.TruncateUntil(checkpointCoveredAofAddress); + db.AppendOnlyFile?.Commit(); + } + + if (db.ObjectStore != null) + { + // During the checkpoint, we may have serialized Garnet objects in (v) versions of objects. + // We can now safely remove these serialized versions as they are no longer needed. + using var iter1 = db.ObjectStore.Log.Scan(db.ObjectStore.Log.ReadOnlyAddress, + db.ObjectStore.Log.TailAddress, ScanBufferingMode.SinglePageBuffering, includeSealedRecords: true); + while (iter1.GetNext(out _, out _, out var value)) + { + if (value != null) + ((GarnetObjectBase)value).serialized = null; + } + } + + logger?.LogInformation("Completed checkpoint"); + } + + protected abstract ref GarnetDatabase GetDatabaseByRef(int dbId); + + /// + /// Asynchronously checkpoint a single database + /// + /// Store type to checkpoint + /// Database to checkpoint + /// Logger + /// Cancellation token + /// Task + protected async Task TakeCheckpointAsync(GarnetDatabase db, StoreType storeType, ILogger logger = null, CancellationToken token = default) + { + try + { + DoCompaction(ref db); + var lastSaveStoreTailAddress = db.MainStore.Log.TailAddress; + var lastSaveObjectStoreTailAddress = (db.ObjectStore?.Log.TailAddress).GetValueOrDefault(); + + var full = db.LastSaveStoreTailAddress == 0 || + lastSaveStoreTailAddress - db.LastSaveStoreTailAddress >= StoreWrapper.serverOptions.FullCheckpointLogInterval || + (db.ObjectStore != null && (db.LastSaveObjectStoreTailAddress == 0 || + lastSaveObjectStoreTailAddress - db.LastSaveObjectStoreTailAddress >= StoreWrapper.serverOptions.FullCheckpointLogInterval)); + + var tryIncremental = StoreWrapper.serverOptions.EnableIncrementalSnapshots; + if (db.MainStore.IncrementalSnapshotTailAddress >= StoreWrapper.serverOptions.IncrementalSnapshotLogSizeLimit) + tryIncremental = false; + if (db.ObjectStore?.IncrementalSnapshotTailAddress >= StoreWrapper.serverOptions.IncrementalSnapshotLogSizeLimit) + tryIncremental = false; + + var checkpointType = StoreWrapper.serverOptions.UseFoldOverCheckpoints ? CheckpointType.FoldOver : CheckpointType.Snapshot; + await InitiateCheckpointAsync(db, full, checkpointType, tryIncremental, storeType, logger); + + if (full) + { + if (storeType is StoreType.Main or StoreType.All) + db.LastSaveStoreTailAddress = lastSaveStoreTailAddress; + if (storeType is StoreType.Object or StoreType.All) + db.LastSaveObjectStoreTailAddress = lastSaveObjectStoreTailAddress; + } + } + catch (Exception ex) + { + logger?.LogError(ex, "Checkpointing threw exception"); + } + } + + protected bool TryPauseCheckpoints(ref GarnetDatabase db) + => db.CheckpointingLock.TryWriteLock(); + + protected void ResumeCheckpoints(ref GarnetDatabase db) + => db.CheckpointingLock.WriteUnlock(); + protected void RecoverDatabaseAOF(ref GarnetDatabase db) { if (db.AppendOnlyFile == null) return; @@ -177,8 +334,125 @@ protected void FlushDatabase(ref GarnetDatabase db, bool unsafeTruncateLog) db.ObjectStore?.Log.ShiftBeginAddress(db.ObjectStore.Log.TailAddress, truncateLog: unsafeTruncateLog); } - public abstract void Dispose(); + private void DoCompaction(ref GarnetDatabase db) + { + // Periodic compaction -> no need to compact before checkpointing + if (StoreWrapper.serverOptions.CompactionFrequencySecs > 0) return; - public abstract IDatabaseManager Clone(bool enableAof); + DoCompaction(ref db, StoreWrapper.serverOptions.CompactionMaxSegments, + StoreWrapper.serverOptions.ObjectStoreCompactionMaxSegments, 1, + StoreWrapper.serverOptions.CompactionType, StoreWrapper.serverOptions.CompactionForceDelete); + } + + private void DoCompaction(ref GarnetDatabase db, int mainStoreMaxSegments, int objectStoreMaxSegments, int numSegmentsToCompact, LogCompactionType compactionType, bool compactionForceDelete) + { + if (compactionType == LogCompactionType.None) return; + + var mainStoreLog = db.MainStore.Log; + + var mainStoreMaxLogSize = (1L << StoreWrapper.serverOptions.SegmentSizeBits()) * mainStoreMaxSegments; + + if (mainStoreLog.ReadOnlyAddress - mainStoreLog.BeginAddress > mainStoreMaxLogSize) + { + var readOnlyAddress = mainStoreLog.ReadOnlyAddress; + var compactLength = (1L << StoreWrapper.serverOptions.SegmentSizeBits()) * (mainStoreMaxSegments - numSegmentsToCompact); + var untilAddress = readOnlyAddress - compactLength; + Logger?.LogInformation( + $"Begin main store compact until {untilAddress}, Begin = {mainStoreLog.BeginAddress}, ReadOnly = {readOnlyAddress}, Tail = {mainStoreLog.TailAddress}"); + + switch (compactionType) + { + case LogCompactionType.Shift: + mainStoreLog.ShiftBeginAddress(untilAddress, true, compactionForceDelete); + break; + + case LogCompactionType.Scan: + mainStoreLog.Compact>(new SpanByteFunctions(), untilAddress, CompactionType.Scan); + if (compactionForceDelete) + { + CompactionCommitAof(ref db); + mainStoreLog.Truncate(); + } + break; + + case LogCompactionType.Lookup: + mainStoreLog.Compact>(new SpanByteFunctions(), untilAddress, CompactionType.Lookup); + if (compactionForceDelete) + { + CompactionCommitAof(ref db); + mainStoreLog.Truncate(); + } + break; + } + + Logger?.LogInformation( + $"End main store compact until {untilAddress}, Begin = {mainStoreLog.BeginAddress}, ReadOnly = {readOnlyAddress}, Tail = {mainStoreLog.TailAddress}"); + } + + if (db.ObjectStore == null) return; + + var objectStoreLog = db.ObjectStore.Log; + + var objectStoreMaxLogSize = (1L << StoreWrapper.serverOptions.ObjectStoreSegmentSizeBits()) * objectStoreMaxSegments; + + if (objectStoreLog.ReadOnlyAddress - objectStoreLog.BeginAddress > objectStoreMaxLogSize) + { + var readOnlyAddress = objectStoreLog.ReadOnlyAddress; + var compactLength = (1L << StoreWrapper.serverOptions.ObjectStoreSegmentSizeBits()) * (objectStoreMaxSegments - numSegmentsToCompact); + var untilAddress = readOnlyAddress - compactLength; + Logger?.LogInformation( + $"Begin object store compact until {untilAddress}, Begin = {objectStoreLog.BeginAddress}, ReadOnly = {readOnlyAddress}, Tail = {objectStoreLog.TailAddress}"); + + switch (compactionType) + { + case LogCompactionType.Shift: + objectStoreLog.ShiftBeginAddress(untilAddress, compactionForceDelete); + break; + + case LogCompactionType.Scan: + objectStoreLog.Compact>( + new SimpleSessionFunctions(), untilAddress, CompactionType.Scan); + if (compactionForceDelete) + { + CompactionCommitAof(ref db); + objectStoreLog.Truncate(); + } + break; + + case LogCompactionType.Lookup: + objectStoreLog.Compact>( + new SimpleSessionFunctions(), untilAddress, CompactionType.Lookup); + if (compactionForceDelete) + { + CompactionCommitAof(ref db); + objectStoreLog.Truncate(); + } + break; + } + + Logger?.LogInformation( + $"End object store compact until {untilAddress}, Begin = {mainStoreLog.BeginAddress}, ReadOnly = {readOnlyAddress}, Tail = {mainStoreLog.TailAddress}"); + } + } + + private void CompactionCommitAof(ref GarnetDatabase db) + { + // If we are the primary, we commit the AOF. + // If we are the replica, we commit the AOF only if fast commit is disabled + // because we do not want to clobber AOF addresses. + // TODO: replica should instead wait until the next AOF commit is done via primary + if (StoreWrapper.serverOptions.EnableAOF) + { + if (StoreWrapper.serverOptions.EnableCluster && StoreWrapper.clusterProvider.IsReplica()) + { + if (!StoreWrapper.serverOptions.EnableFastCommit) + db.AppendOnlyFile?.CommitAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + } + else + { + db.AppendOnlyFile?.CommitAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + } + } + } } } diff --git a/libs/server/Databases/IDatabaseManager.cs b/libs/server/Databases/IDatabaseManager.cs index b2edebe642..cf6ba4b471 100644 --- a/libs/server/Databases/IDatabaseManager.cs +++ b/libs/server/Databases/IDatabaseManager.cs @@ -2,6 +2,9 @@ // Licensed under the MIT license. using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Tsavorite.core; namespace Garnet.server @@ -62,8 +65,61 @@ public interface IDatabaseManager : IDisposable /// True if database was retrieved or added successfully public bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db); + /// + /// Mark the beginning of a checkpoint by taking and a lock to avoid concurrent checkpointing + /// + /// ID of database to lock + /// True if lock acquired + public bool TryPauseCheckpoints(int dbId); + + /// + /// Release checkpoint task lock + /// + /// ID of database to unlock + public void ResumeCheckpoints(int dbId); + public void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStoreFromToken = false, bool recoverObjectStoreFromToken = false, CheckpointMetadata metadata = null); + /// + /// Take checkpoint of all active databases + /// + /// True if method can return before checkpoint is taken + /// Store type to checkpoint + /// Logger + /// Cancellation token + /// False if another checkpointing process is already in progress + public bool TakeCheckpoint(bool background, StoreType storeType = StoreType.All, ILogger logger = null, CancellationToken token = default); + + /// + /// Take checkpoint of specified database ID + /// + /// True if method can return before checkpoint is taken + /// ID of database to checkpoint + /// Store type to checkpoint + /// Logger + /// Cancellation token + /// False if another checkpointing process is already in progress + public bool TakeCheckpoint(bool background, int dbId, StoreType storeType = StoreType.All, + ILogger logger = null, CancellationToken token = default); + + /// + /// Take a checkpoint if no checkpoint was taken after the provided time offset + /// + /// Time offset + /// ID of database to checkpoint (default: DB 0) + /// Task + public Task TakeOnDemandCheckpointAsync(DateTimeOffset entryTime, int dbId = 0); + + /// + /// Take a checkpoint of all active databases whose AOF size has reached a specified limit + /// + /// AOF size limit + /// Cancellation token + /// Logger + /// Task + public Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLimit, CancellationToken token = default, + ILogger logger = null); + /// /// Recover AOF /// @@ -109,6 +165,14 @@ public interface IDatabaseManager : IDisposable /// Truncate log public void FlushAllDatabases(bool unsafeTruncateLog); + /// + /// Try to swap between two database instances + /// + /// First database ID + /// Second database ID + /// True if swap successful + public bool TrySwapDatabases(int dbId1, int dbId2); + /// /// Create a shallow copy of the IDatabaseManager instance and copy databases to the new instance /// @@ -116,6 +180,6 @@ public interface IDatabaseManager : IDisposable /// public IDatabaseManager Clone(bool enableAof); - internal FunctionsState CreateFunctionsState(CustomCommandManager customCommandManager, GarnetObjectSerializer garnetObjectSerializer, int dbId = 0); + internal FunctionsState CreateFunctionsState(int dbId = 0); } } diff --git a/libs/server/Databases/MultiDatabaseManager.cs b/libs/server/Databases/MultiDatabaseManager.cs index 9c8163b676..a416e7d1f6 100644 --- a/libs/server/Databases/MultiDatabaseManager.cs +++ b/libs/server/Databases/MultiDatabaseManager.cs @@ -42,9 +42,13 @@ internal class MultiDatabaseManager : DatabaseManagerBase // Used by recurring checkpointing task if multiple DBs exist Task[] checkpointTasks; + // Reusable array for storing database IDs for checkpointing + int[] dbIdsToCheckpoint = null; + // Reusable task array for tracking aof commits of multiple DBs // Used by recurring aof commits task if multiple DBs exist Task[] aofTasks; + readonly object activeDbIdsLock = new(); // Path of serialization for the DB IDs file used when committing / recovering to / from AOF @@ -57,11 +61,11 @@ internal class MultiDatabaseManager : DatabaseManagerBase readonly string checkpointDirBaseName; - public MultiDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabaseDelegate, StoreWrapper storeWrapper, int maxDatabases, - ILoggerFactory loggerFactory = null, bool createDefaultDatabase = true) : base(createsDatabaseDelegate, storeWrapper, loggerFactory) + public MultiDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabaseDelegate, + StoreWrapper storeWrapper, bool createDefaultDatabase = true) : base(createsDatabaseDelegate, storeWrapper) { - this.maxDatabases = maxDatabases; - this.Logger = loggerFactory?.CreateLogger(nameof(MultiDatabaseManager)); + this.maxDatabases = storeWrapper.serverOptions.MaxDatabases; + this.Logger = storeWrapper.loggerFactory?.CreateLogger(nameof(MultiDatabaseManager)); // Create default databases map of size 1 databases = new ExpandableMap(1, 0, this.maxDatabases - 1); @@ -89,11 +93,12 @@ public MultiDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabase } public MultiDatabaseManager(MultiDatabaseManager src, bool enableAof) : this(src.CreateDatabaseDelegate, - src.StoreWrapper, src.maxDatabases, src.LoggerFactory, false) + src.StoreWrapper, createDefaultDatabase: false) { this.CopyDatabases(src, enableAof); } + /// public override void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStoreFromToken = false, bool recoverObjectStoreFromToken = false, CheckpointMetadata metadata = null) { if (replicaRecover) @@ -140,6 +145,118 @@ public override void RecoverCheckpoint(bool replicaRecover = false, bool recover } } + /// + public override bool TakeCheckpoint(bool background, StoreType storeType = StoreType.All, ILogger logger = null, + CancellationToken token = default) + { + var lockAcquired = TryGetDatabasesReadLockAsync(token).Result; + if (!lockAcquired) return false; + + try + { + var activeDbIdsSize = activeDbIdsLength; + Array.Copy(activeDbIds, dbIdsToCheckpoint, activeDbIdsSize); + + TakeDatabasesCheckpointAsync(storeType, activeDbIdsSize, logger: logger, token: token).GetAwaiter().GetResult(); + } + finally + { + databasesLock.ReadUnlock(); + } + + return true; + } + + /// + public override bool TakeCheckpoint(bool background, int dbId, StoreType storeType = StoreType.All, ILogger logger = null, + CancellationToken token = default) + { + var databasesMapSize = databases.ActualSize; + var databasesMapSnapshot = databases.Map; + Debug.Assert(dbId < databasesMapSize && !databasesMapSnapshot[dbId].IsDefault()); + + if (!TryPauseCheckpointsContinuousAsync(dbId, token).GetAwaiter().GetResult()) + return false; + + var checkpointTask = TakeCheckpointAsync(databasesMapSnapshot[dbId], storeType, logger: logger, token: token).ContinueWith( + _ => + { + databasesMapSnapshot[dbId].LastSaveTime = DateTimeOffset.UtcNow; + ResumeCheckpoints(dbId); + }, TaskContinuationOptions.ExecuteSynchronously).GetAwaiter(); + + if (background) + return true; + + checkpointTask.GetResult(); + return true; + } + + /// + public override async Task TakeOnDemandCheckpointAsync(DateTimeOffset entryTime, int dbId = 0) + { + var databasesMapSize = databases.ActualSize; + var databasesMapSnapshot = databases.Map; + Debug.Assert(dbId < databasesMapSize && !databasesMapSnapshot[dbId].IsDefault()); + + // Take lock to ensure no other task will be taking a checkpoint + var checkpointsPaused = TryPauseCheckpoints(dbId); + + try + { + // If an external task has taken a checkpoint beyond the provided entryTime return + if (!checkpointsPaused || databasesMapSnapshot[dbId].LastSaveTime > entryTime) + return; + + // Necessary to take a checkpoint because the latest checkpoint is before entryTime + await TakeCheckpointAsync(databasesMapSnapshot[dbId], StoreType.All, logger: Logger); + + databasesMapSnapshot[dbId].LastSaveTime = DateTimeOffset.UtcNow; + } + finally + { + ResumeCheckpoints(dbId); + } + } + + public override async Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLimit, CancellationToken token = default, + ILogger logger = null) + { + var lockAcquired = await TryGetDatabasesReadLockAsync(token); + if (!lockAcquired) return; + + try + { + var databasesMapSnapshot = databases.Map; + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + + var dbIdsIdx = 0; + for (var i = 0; i < activeDbIdsSize; i++) + { + var dbId = activeDbIdsSnapshot[i]; + var db = databasesMapSnapshot[dbId]; + Debug.Assert(!db.IsDefault()); + + var dbAofSize = db.AppendOnlyFile.TailAddress - db.AppendOnlyFile.BeginAddress; + if (dbAofSize > aofSizeLimit) + { + logger?.LogInformation($"Enforcing AOF size limit currentAofSize: {dbAofSize} > AofSizeLimit: {aofSizeLimit} (Database ID: {dbId})"); + dbIdsToCheckpoint[dbIdsIdx++] = dbId; + break; + } + } + + if (dbIdsIdx == 0) return; + + await TakeDatabasesCheckpointAsync(StoreType.All, dbIdsIdx, logger: logger, token: token); + } + finally + { + databasesLock.ReadUnlock(); + } + } + /// public override void RecoverAOF() { @@ -165,6 +282,7 @@ public override void RecoverAOF() } } + /// public override long ReplayAOF(long untilAddress = -1) { if (!StoreWrapper.serverOptions.EnableAOF) @@ -197,6 +315,7 @@ public override long ReplayAOF(long untilAddress = -1) return replicationOffset; } + /// public override void Reset(int dbId = 0) { if (!this.TryGetOrAddDatabase(dbId, out var db)) @@ -213,15 +332,47 @@ public override void EnqueueCommit(bool isMainStore, long version, int dbId = 0) EnqueueDatabaseCommit(ref db, isMainStore, version); } + /// + public override bool TrySwapDatabases(int dbId1, int dbId2) + { + if (!this.TryGetOrAddDatabase(dbId1, out var db1) || + !this.TryGetOrAddDatabase(dbId2, out var db2)) + return false; + + databasesLock.WriteLock(); + try + { + var databaseMapSnapshot = this.databases.Map; + databaseMapSnapshot[dbId2] = db1; + databaseMapSnapshot[dbId1] = db2; + } + finally + { + databasesLock.WriteUnlock(); + } + + return true; + } + + /// public override IDatabaseManager Clone(bool enableAof) => new MultiDatabaseManager(this, enableAof); - public override FunctionsState CreateFunctionsState(CustomCommandManager customCommandManager, GarnetObjectSerializer garnetObjectSerializer, int dbId = 0) + public override FunctionsState CreateFunctionsState(int dbId = 0) { if (!this.TryGetOrAddDatabase(dbId, out var db)) throw new GarnetException($"Database with ID {dbId} was not found."); - return new(db.AppendOnlyFile, db.VersionMap, customCommandManager, null, db.ObjectStoreSizeTracker, - garnetObjectSerializer); + return new(db.AppendOnlyFile, db.VersionMap, StoreWrapper.customCommandManager, null, db.ObjectStoreSizeTracker, + StoreWrapper.GarnetObjectSerializer); + } + + protected override ref GarnetDatabase GetDatabaseByRef(int dbId) + { + var databasesMapSize = databases.ActualSize; + var databasesMapSnapshot = databases.Map; + + Debug.Assert(dbId < databasesMapSize); + return ref databasesMapSnapshot[dbId]; } /// @@ -236,6 +387,42 @@ public override bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db) return true; } + /// + public override bool TryPauseCheckpoints(int dbId = 0) + { + if (!this.TryGetOrAddDatabase(dbId, out var db)) + throw new GarnetException($"Database with ID {dbId} was not found."); + + return TryPauseCheckpoints(ref db); + } + + /// + public override async Task TryPauseCheckpointsContinuousAsync(int dbId = 0, + CancellationToken token = default) + { + if (!this.TryGetOrAddDatabase(dbId, out var db)) + throw new GarnetException($"Database with ID {dbId} was not found."); + + var checkpointsPaused = TryPauseCheckpoints(ref db); + + while (!checkpointsPaused && !token.IsCancellationRequested && !Disposed) + { + await Task.Yield(); + checkpointsPaused = TryPauseCheckpoints(ref db); + } + + return checkpointsPaused; + } + + /// + public override void ResumeCheckpoints(int dbId = 0) + { + if (!this.TryGetOrAddDatabase(dbId, out var db)) + throw new GarnetException($"Database with ID {dbId} was not found."); + + ResumeCheckpoints(ref db); + } + /// public override bool TryGetDatabase(int dbId, out GarnetDatabase db) { @@ -283,6 +470,24 @@ public override void FlushAllDatabases(bool unsafeTruncateLog) } } + /// + /// Continuously try to take a database read lock that ensures no db swap operations occur + /// + /// Cancellation token + /// True if lock acquired + public async Task TryGetDatabasesReadLockAsync(CancellationToken token = default) + { + var lockAcquired = databasesLock.TryReadLock(); + + while (!lockAcquired && !token.IsCancellationRequested && !Disposed) + { + await Task.Yield(); + lockAcquired = databasesLock.TryReadLock(); + } + + return lockAcquired; + } + /// /// Try to add a new database /// @@ -345,6 +550,7 @@ private void HandleDatabaseAdded(int dbId) activeDbIdsLength = dbIdIdx + 1; checkpointTasks = new Task[activeDbIdsLength]; aofTasks = new Task[activeDbIdsLength]; + dbIdsToCheckpoint = new int[activeDbIdsLength]; } } @@ -384,7 +590,7 @@ private bool TryGetSavedDatabaseIds(string path, string baseName, out int[] dbId /// /// Source IDatabaseManager /// True if should enable AOF in copied databases - protected void CopyDatabases(IDatabaseManager src, bool enableAof) + private void CopyDatabases(IDatabaseManager src, bool enableAof) { switch (src) { @@ -410,6 +616,54 @@ protected void CopyDatabases(IDatabaseManager src, bool enableAof) } } + /// + /// Asynchronously checkpoint multiple databases and wait for all to complete + /// + /// Store type to checkpoint + /// Number of databases to checkpoint (first dbIdsCount indexes from dbIdsToCheckpoint) + /// Logger + /// Cancellation token + /// False if checkpointing already in progress + private async Task TakeDatabasesCheckpointAsync(StoreType storeType, int dbIdsCount, ILogger logger = null, + CancellationToken token = default) + { + Debug.Assert(checkpointTasks != null); + Debug.Assert(dbIdsCount <= dbIdsToCheckpoint.Length); + + for (var i = 0; i < checkpointTasks.Length; i++) + checkpointTasks[i] = Task.CompletedTask; + + var databaseMapSnapshot = databases.Map; + + var currIdx = 0; + while (currIdx < dbIdsCount) + { + var dbId = dbIdsToCheckpoint[currIdx]; + + // Prevent parallel checkpoint + if (!await TryPauseCheckpointsContinuousAsync(dbId, token)) + continue; + + checkpointTasks[currIdx] = TakeCheckpointAsync(databaseMapSnapshot[dbId], storeType, logger: logger, token: token).ContinueWith( + _ => + { + databaseMapSnapshot[dbId].LastSaveTime = DateTimeOffset.UtcNow; + ResumeCheckpoints(dbId); + }, TaskContinuationOptions.ExecuteSynchronously); + + currIdx++; + } + + try + { + await Task.WhenAll(checkpointTasks); + } + catch (Exception ex) + { + logger?.LogError(ex, $"Checkpointing threw exception"); + } + } + public override void Dispose() { if (Disposed) return; diff --git a/libs/server/Databases/SingleDatabaseManager.cs b/libs/server/Databases/SingleDatabaseManager.cs index 3c9c5baebf..7790269f7a 100644 --- a/libs/server/Databases/SingleDatabaseManager.cs +++ b/libs/server/Databases/SingleDatabaseManager.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. using System; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Tsavorite.core; @@ -17,10 +19,10 @@ internal class SingleDatabaseManager : DatabaseManagerBase GarnetDatabase defaultDatabase; - public SingleDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabaseDelegate, StoreWrapper storeWrapper, ILoggerFactory loggerFactory = null, bool createDefaultDatabase = true) : + public SingleDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabaseDelegate, StoreWrapper storeWrapper, bool createDefaultDatabase = true) : base(createsDatabaseDelegate, storeWrapper) { - this.Logger = loggerFactory?.CreateLogger(nameof(SingleDatabaseManager)); + this.Logger = storeWrapper.loggerFactory?.CreateLogger(nameof(SingleDatabaseManager)); // Create default database of index 0 (unless specified otherwise) if (createDefaultDatabase) @@ -29,7 +31,7 @@ public SingleDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabas } } - public SingleDatabaseManager(SingleDatabaseManager src, bool enableAof) : this(src.CreateDatabaseDelegate, src.StoreWrapper, src.LoggerFactory) + public SingleDatabaseManager(SingleDatabaseManager src, bool enableAof) : this(src.CreateDatabaseDelegate, src.StoreWrapper, createDefaultDatabase: false) { this.CopyDatabases(src, enableAof); } @@ -86,6 +88,119 @@ public override void RecoverCheckpoint(bool replicaRecover = false, bool recover } } + /// + public override bool TryPauseCheckpoints(int dbId) + { + ArgumentOutOfRangeException.ThrowIfNotEqual(dbId, 0); + + return TryPauseCheckpoints(ref DefaultDatabase); + } + + /// + public override async Task TryPauseCheckpointsContinuousAsync(int dbId, + CancellationToken token = default) + { + ArgumentOutOfRangeException.ThrowIfNotEqual(dbId, 0); + + var checkpointsPaused = TryPauseCheckpoints(ref DefaultDatabase); + + while (!checkpointsPaused && !token.IsCancellationRequested && !Disposed) + { + await Task.Yield(); + checkpointsPaused = TryPauseCheckpoints(ref DefaultDatabase); + } + + return checkpointsPaused; + } + + /// + public override void ResumeCheckpoints(int dbId) + { + ArgumentOutOfRangeException.ThrowIfNotEqual(dbId, 0); + + ResumeCheckpoints(ref DefaultDatabase); + } + + /// + public override bool TakeCheckpoint(bool background, StoreType storeType = StoreType.All, ILogger logger = null, + CancellationToken token = default) + { + if (!TryPauseCheckpointsContinuousAsync(DefaultDatabase.Id, token: token).GetAwaiter().GetResult()) + return false; + + var checkpointTask = TakeCheckpointAsync(DefaultDatabase, storeType, logger: logger, token: token).ContinueWith( + _ => + { + DefaultDatabase.LastSaveTime = DateTimeOffset.UtcNow; + ResumeCheckpoints(DefaultDatabase.Id); + }, TaskContinuationOptions.ExecuteSynchronously).GetAwaiter(); + + if (background) + return true; + + checkpointTask.GetResult(); + return true; + } + + /// + public override bool TakeCheckpoint(bool background, int dbId, StoreType storeType = StoreType.All, + ILogger logger = null, + CancellationToken token = default) + { + ArgumentOutOfRangeException.ThrowIfNotEqual(dbId, 0); + + return TakeCheckpoint(background, storeType, logger, token); + } + + /// + public override async Task TakeOnDemandCheckpointAsync(DateTimeOffset entryTime, int dbId = 0) + { + ArgumentOutOfRangeException.ThrowIfNotEqual(dbId, 0); + + // Take lock to ensure no other task will be taking a checkpoint + var checkpointsPaused = TryPauseCheckpoints(dbId); + + try + { + // If an external task has taken a checkpoint beyond the provided entryTime return + if (!checkpointsPaused || DefaultDatabase.LastSaveTime > entryTime) + return; + + // Necessary to take a checkpoint because the latest checkpoint is before entryTime + await TakeCheckpointAsync(DefaultDatabase, StoreType.All, logger: Logger); + + DefaultDatabase.LastSaveTime = DateTimeOffset.UtcNow; + } + finally + { + ResumeCheckpoints(dbId); + } + } + + /// + public override async Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLimit, + CancellationToken token = default, ILogger logger = null) + { + var aofSize = AppendOnlyFile.TailAddress - AppendOnlyFile.BeginAddress; + if (aofSize <= aofSizeLimit) return; + + if (!TryPauseCheckpointsContinuousAsync(DefaultDatabase.Id, token: token).GetAwaiter().GetResult()) + return; + + logger?.LogInformation($"Enforcing AOF size limit currentAofSize: {aofSize} > AofSizeLimit: {aofSizeLimit}"); + + try + { + await TakeCheckpointAsync(DefaultDatabase, StoreType.All, logger: logger, token: token); + + DefaultDatabase.LastSaveTime = DateTimeOffset.UtcNow; + } + finally + { + ResumeCheckpoints(DefaultDatabase.Id); + } + } + /// public override void RecoverAOF() => RecoverDatabaseAOF(ref DefaultDatabase); @@ -134,6 +249,7 @@ public override bool TryGetDatabase(int dbId, out GarnetDatabase db) return true; } + /// public override void FlushDatabase(bool unsafeTruncateLog, int dbId = 0) { ArgumentOutOfRangeException.ThrowIfNotEqual(dbId, 0); @@ -141,16 +257,29 @@ public override void FlushDatabase(bool unsafeTruncateLog, int dbId = 0) FlushDatabase(ref DefaultDatabase, unsafeTruncateLog); } + /// public override void FlushAllDatabases(bool unsafeTruncateLog) => FlushDatabase(ref DefaultDatabase, unsafeTruncateLog); + /// + public override bool TrySwapDatabases(int dbId1, int dbId2) => false; + + /// public override IDatabaseManager Clone(bool enableAof) => new SingleDatabaseManager(this, enableAof); - public override FunctionsState CreateFunctionsState(CustomCommandManager customCommandManager, GarnetObjectSerializer garnetObjectSerializer, int dbId = 0) + public override FunctionsState CreateFunctionsState(int dbId = 0) + { + ArgumentOutOfRangeException.ThrowIfNotEqual(dbId, 0); + + return new(AppendOnlyFile, VersionMap, StoreWrapper.customCommandManager, null, ObjectStoreSizeTracker, + StoreWrapper.GarnetObjectSerializer); + } + + protected override ref GarnetDatabase GetDatabaseByRef(int dbId) { ArgumentOutOfRangeException.ThrowIfNotEqual(dbId, 0); - return new(AppendOnlyFile, VersionMap, customCommandManager, null, ObjectStoreSizeTracker, garnetObjectSerializer); + return ref DefaultDatabase; } private void CopyDatabases(SingleDatabaseManager src, bool enableAof) diff --git a/libs/server/GarnetDatabase.cs b/libs/server/GarnetDatabase.cs index 797f8f6ecc..5232f0a4e0 100644 --- a/libs/server/GarnetDatabase.cs +++ b/libs/server/GarnetDatabase.cs @@ -1,5 +1,6 @@ using System; using System.Threading; +using Garnet.common; using Tsavorite.core; namespace Garnet.server @@ -71,10 +72,21 @@ public struct GarnetDatabase : IDisposable /// public DateTimeOffset LastSaveTime; + /// + /// True if database's main store index has maxed-out + /// public bool MainStoreIndexMaxedOut; - + + /// + /// True if database's object store index has maxed-out + /// public bool ObjectStoreIndexMaxedOut; + /// + /// Reader-Writer lock for database checkpointing + /// + public SingleWriterMultiReaderLock CheckpointingLock; + bool disposed = false; public GarnetDatabase(int id, TsavoriteKV mainStore, @@ -129,6 +141,9 @@ public void Dispose() Thread.Yield(); } + // Wait for checkpoints to complete and disable checkpointing + CheckpointingLock.CloseLock(); + disposed = true; } } diff --git a/libs/server/Resp/AdminCommands.cs b/libs/server/Resp/AdminCommands.cs index 88dcc3545d..ec9c4773fb 100644 --- a/libs/server/Resp/AdminCommands.cs +++ b/libs/server/Resp/AdminCommands.cs @@ -878,7 +878,7 @@ private bool NetworkSAVE() } } - if (!storeWrapper.TakeCheckpoint(false, dbId: dbId, logger: logger)) + if (!storeWrapper.databaseManager.TakeCheckpoint(false, dbId: dbId, logger: logger)) { while (!RespWriteUtils.TryWriteError("ERR checkpoint already in progress"u8, ref dcurr, dend)) SendAndReset(); @@ -932,7 +932,7 @@ private bool NetworkLASTSAVE() } } - storeWrapper.TryGetDatabase(dbId, out var db); + storeWrapper.databaseManager.TryGetDatabase(dbId, out var db); var seconds = db.LastSaveTime.ToUnixTimeSeconds(); while (!RespWriteUtils.TryWriteInt64(seconds, ref dcurr, dend)) @@ -989,7 +989,7 @@ private bool NetworkBGSAVE() } } - var success = storeWrapper.TakeCheckpoint(true, dbId: dbId, logger: logger); + var success = storeWrapper.databaseManager.TakeCheckpoint(true, dbId: dbId, logger: logger); if (success) { while (!RespWriteUtils.TryWriteSimpleString("Background saving started"u8, ref dcurr, dend)) diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index 4cf9d63a64..09d677f9d5 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -1670,10 +1670,10 @@ void ExecuteFlushDb(RespCommand cmd, bool unsafeTruncateLog) switch (cmd) { case RespCommand.FLUSHDB: - storeWrapper.FlushDatabase(unsafeTruncateLog, activeDbId); + storeWrapper.databaseManager.FlushDatabase(unsafeTruncateLog, activeDbId); break; case RespCommand.FLUSHALL: - storeWrapper.FlushAllDatabases(unsafeTruncateLog); + storeWrapper.databaseManager.FlushAllDatabases(unsafeTruncateLog); break; } } diff --git a/libs/server/Servers/StoreApi.cs b/libs/server/Servers/StoreApi.cs index d0aa937e5d..cdbdf28a48 100644 --- a/libs/server/Servers/StoreApi.cs +++ b/libs/server/Servers/StoreApi.cs @@ -48,13 +48,7 @@ public StoreApi(StoreWrapper storeWrapper) /// Optionally truncate log on disk. This is a destructive operation. Instead take a checkpoint if you are using checkpointing, as /// that will safely truncate the log on disk after the checkpoint. /// - public void FlushDB(int dbId = 0, bool unsafeTruncateLog = false) - { - var dbFound = storeWrapper.TryGetDatabase(dbId, out var db); - if (!dbFound) return; - - db.MainStore.Log.ShiftBeginAddress(db.MainStore.Log.TailAddress, truncateLog: unsafeTruncateLog); - db.ObjectStore?.Log.ShiftBeginAddress(db.ObjectStore.Log.TailAddress, truncateLog: unsafeTruncateLog); - } + public void FlushDB(int dbId = 0, bool unsafeTruncateLog = false) => + storeWrapper.databaseManager.FlushDatabase(unsafeTruncateLog, dbId); } } \ No newline at end of file diff --git a/libs/server/Storage/Session/StorageSession.cs b/libs/server/Storage/Session/StorageSession.cs index 86c2437c14..8db18f279b 100644 --- a/libs/server/Storage/Session/StorageSession.cs +++ b/libs/server/Storage/Session/StorageSession.cs @@ -69,11 +69,11 @@ public StorageSession(StoreWrapper storeWrapper, parseState.Initialize(); - functionsState = storeWrapper.CreateFunctionsState(dbId); + functionsState = storeWrapper.databaseManager.CreateFunctionsState(dbId); var functions = new MainSessionFunctions(functionsState); - var dbFound = storeWrapper.TryGetDatabase(dbId, out var db); + var dbFound = storeWrapper.databaseManager.TryGetDatabase(dbId, out var db); Debug.Assert(dbFound); var session = db.MainStore.NewSession(functions); diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index f3db734a92..a2b027e79a 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -132,7 +132,6 @@ public sealed class StoreWrapper internal readonly string runId; readonly CancellationTokenSource ctsCommit; - SingleWriterMultiReaderLock checkpointTaskLock; // True if this server supports more than one logical database readonly bool allowMultiDb; @@ -147,7 +146,7 @@ public StoreWrapper( string version, string redisProtocolVersion, IGarnetServer server, - IDatabaseManager databaseManager, + DatabaseCreatorDelegate createDatabaseDelegate, CustomCommandManager customCommandManager, GarnetServerOptions serverOptions, SubscribeBroker subscribeBroker, @@ -162,7 +161,9 @@ public StoreWrapper( this.serverOptions = serverOptions; this.subscribeBroker = subscribeBroker; this.customCommandManager = customCommandManager; - this.databaseManager = databaseManager; + this.databaseManager = serverOptions.EnableCluster || serverOptions.MaxDatabases == 1 + ? new SingleDatabaseManager(createDatabaseDelegate, this) + : new MultiDatabaseManager(createDatabaseDelegate, this); this.monitor = serverOptions.MetricsSamplingFrequency > 0 ? new GarnetServerMonitor(this, serverOptions, server, loggerFactory?.CreateLogger("GarnetServerMonitor")) @@ -233,7 +234,7 @@ public StoreWrapper(StoreWrapper storeWrapper, bool recordToAof) : this( storeWrapper.version, storeWrapper.redisProtocolVersion, storeWrapper.server, - storeWrapper.databaseManager.Clone(recordToAof), + null, storeWrapper.customCommandManager, storeWrapper.serverOptions, storeWrapper.subscribeBroker, @@ -241,6 +242,7 @@ public StoreWrapper(StoreWrapper storeWrapper, bool recordToAof) : this( null, storeWrapper.loggerFactory) { + this.databaseManager = storeWrapper.databaseManager.Clone(recordToAof); } /// @@ -286,112 +288,60 @@ internal void Recover() { if (serverOptions.Recover) { - databaseManager.RecoverCheckpoint(); - databaseManager.RecoverAOF(); + RecoverCheckpoint(); + RecoverAOF(); ReplayAOF(); } } } + public void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStoreFromToken = false, + bool recoverObjectStoreFromToken = false, CheckpointMetadata metadata = null) + => databaseManager.RecoverCheckpoint(replicaRecover, recoverMainStoreFromToken, recoverObjectStoreFromToken, metadata); + + /// + /// Recover AOF + /// + public void RecoverAOF() => databaseManager.RecoverAOF(); + /// /// When replaying AOF we do not want to write AOF records again. /// public long ReplayAOF(long untilAddress = -1) => this.databaseManager.ReplayAOF(); + /// + /// Append a checkpoint commit to the AOF + /// + /// + /// + /// + public void EnqueueCommit(bool isMainStore, long version, int dbId = 0) => + this.databaseManager.EnqueueCommit(isMainStore, version, dbId); + + /// + /// Reset + /// + /// Database ID + public void Reset(int dbId = 0) => databaseManager.Reset(dbId); + /// /// Try to swap between two database instances /// /// First database ID /// Second database ID /// True if swap successful - public bool TrySwapDatabases(int dbId1, int dbId2) - { - if (!allowMultiDb) return false; - - if (!this.TryGetOrAddDatabase(dbId1, out var db1) || - !this.TryGetOrAddDatabase(dbId2, out var db2)) - return false; - - databasesLock.WriteLock(); - try - { - var databaseMapSnapshot = this.databases.Map; - databaseMapSnapshot[dbId2] = db1; - databaseMapSnapshot[dbId1] = db2; - } - finally - { - databasesLock.WriteUnlock(); - } - - return true; - } + public bool TrySwapDatabases(int dbId1, int dbId2) => this.databaseManager.TrySwapDatabases(dbId1, dbId2); async Task AutoCheckpointBasedOnAofSizeLimit(long aofSizeLimit, CancellationToken token = default, ILogger logger = null) { try { - int[] dbIdsToCheckpoint = null; - while (true) { await Task.Delay(1000, token); if (token.IsCancellationRequested) break; - var aofSizeAtLimit = -1L; - var activeDbIdsSize = activeDbIdsLength; - - if (!allowMultiDb || activeDbIdsSize == 1) - { - var dbAofSize = appendOnlyFile.TailAddress - appendOnlyFile.BeginAddress; - if (dbAofSize > aofSizeLimit) - aofSizeAtLimit = dbAofSize; - - if (aofSizeAtLimit != -1) - { - logger?.LogInformation("Enforcing AOF size limit currentAofSize: {currAofSize} > AofSizeLimit: {AofSizeLimit}", aofSizeAtLimit, aofSizeLimit); - await CheckpointTask(StoreType.All, logger: logger, token: token); - } - - return; - } - - var lockAcquired = TryGetDatabasesReadLockAsync(token).Result; - if (!lockAcquired) continue; - - try - { - var databasesMapSnapshot = databases.Map; - var activeDbIdsSnapshot = activeDbIds; - - if (dbIdsToCheckpoint == null || dbIdsToCheckpoint.Length < activeDbIdsSize) - dbIdsToCheckpoint = new int[activeDbIdsSize]; - - var dbIdsIdx = 0; - for (var i = 0; i < activeDbIdsSize; i++) - { - var dbId = activeDbIdsSnapshot[i]; - var db = databasesMapSnapshot[dbId]; - Debug.Assert(!db.IsDefault()); - - var dbAofSize = db.AppendOnlyFile.TailAddress - db.AppendOnlyFile.BeginAddress; - if (dbAofSize > aofSizeLimit) - { - dbIdsToCheckpoint[dbIdsIdx++] = dbId; - break; - } - } - - if (dbIdsIdx > 0) - { - logger?.LogInformation("Enforcing AOF size limit currentAofSize: {currAofSize} > AofSizeLimit: {AofSizeLimit}", aofSizeAtLimit, aofSizeLimit); - CheckpointDatabases(StoreType.All, ref dbIdsToCheckpoint, ref checkpointTasks, logger: logger, token: token); - } - } - finally - { - databasesLock.ReadUnlock(); - } + await databaseManager.TaskCheckpointBasedOnAofSizeLimitAsync(aofSizeLimit, token, logger); } } catch (Exception ex) @@ -562,127 +512,6 @@ static void ExecuteHashCollect(ScratchBufferManager scratchBufferManager, Storag } } - void DoCompaction(ref GarnetDatabase db) - { - // Periodic compaction -> no need to compact before checkpointing - if (serverOptions.CompactionFrequencySecs > 0) return; - - DoCompaction(ref db, serverOptions.CompactionMaxSegments, serverOptions.ObjectStoreCompactionMaxSegments, 1, serverOptions.CompactionType, serverOptions.CompactionForceDelete); - } - - void DoCompaction(ref GarnetDatabase db, int mainStoreMaxSegments, int objectStoreMaxSegments, int numSegmentsToCompact, LogCompactionType compactionType, bool compactionForceDelete) - { - if (compactionType == LogCompactionType.None) return; - - var mainStoreLog = db.MainStore.Log; - - long mainStoreMaxLogSize = (1L << serverOptions.SegmentSizeBits()) * mainStoreMaxSegments; - - if (mainStoreLog.ReadOnlyAddress - mainStoreLog.BeginAddress > mainStoreMaxLogSize) - { - long readOnlyAddress = mainStoreLog.ReadOnlyAddress; - long compactLength = (1L << serverOptions.SegmentSizeBits()) * (mainStoreMaxSegments - numSegmentsToCompact); - long untilAddress = readOnlyAddress - compactLength; - logger?.LogInformation("Begin main store compact until {untilAddress}, Begin = {beginAddress}, ReadOnly = {readOnlyAddress}, Tail = {tailAddress}", untilAddress, mainStoreLog.BeginAddress, readOnlyAddress, mainStoreLog.TailAddress); - - switch (compactionType) - { - case LogCompactionType.Shift: - mainStoreLog.ShiftBeginAddress(untilAddress, true, compactionForceDelete); - break; - - case LogCompactionType.Scan: - mainStoreLog.Compact>(new SpanByteFunctions(), untilAddress, CompactionType.Scan); - if (compactionForceDelete) - { - CompactionCommitAof(ref db); - mainStoreLog.Truncate(); - } - break; - - case LogCompactionType.Lookup: - mainStoreLog.Compact>(new SpanByteFunctions(), untilAddress, CompactionType.Lookup); - if (compactionForceDelete) - { - CompactionCommitAof(ref db); - mainStoreLog.Truncate(); - } - break; - - default: - break; - } - - logger?.LogInformation("End main store compact until {untilAddress}, Begin = {beginAddress}, ReadOnly = {readOnlyAddress}, Tail = {tailAddress}", untilAddress, mainStoreLog.BeginAddress, readOnlyAddress, mainStoreLog.TailAddress); - } - - if (db.ObjectStore == null) return; - - var objectStoreLog = db.ObjectStore.Log; - - long objectStoreMaxLogSize = (1L << serverOptions.ObjectStoreSegmentSizeBits()) * objectStoreMaxSegments; - - if (objectStoreLog.ReadOnlyAddress - objectStoreLog.BeginAddress > objectStoreMaxLogSize) - { - long readOnlyAddress = objectStoreLog.ReadOnlyAddress; - long compactLength = (1L << serverOptions.ObjectStoreSegmentSizeBits()) * (objectStoreMaxSegments - numSegmentsToCompact); - long untilAddress = readOnlyAddress - compactLength; - logger?.LogInformation("Begin object store compact until {untilAddress}, Begin = {beginAddress}, ReadOnly = {readOnlyAddress}, Tail = {tailAddress}", untilAddress, objectStoreLog.BeginAddress, readOnlyAddress, objectStoreLog.TailAddress); - - switch (compactionType) - { - case LogCompactionType.Shift: - objectStoreLog.ShiftBeginAddress(untilAddress, compactionForceDelete); - break; - - case LogCompactionType.Scan: - objectStoreLog.Compact>( - new SimpleSessionFunctions(), untilAddress, CompactionType.Scan); - if (compactionForceDelete) - { - CompactionCommitAof(ref db); - objectStoreLog.Truncate(); - } - break; - - case LogCompactionType.Lookup: - objectStoreLog.Compact>( - new SimpleSessionFunctions(), untilAddress, CompactionType.Lookup); - if (compactionForceDelete) - { - CompactionCommitAof(ref db); - objectStoreLog.Truncate(); - } - break; - - default: - break; - } - - logger?.LogInformation("End object store compact until {untilAddress}, Begin = {beginAddress}, ReadOnly = {readOnlyAddress}, Tail = {tailAddress}", untilAddress, mainStoreLog.BeginAddress, readOnlyAddress, mainStoreLog.TailAddress); - } - } - - void CompactionCommitAof(ref GarnetDatabase db) - { - // If we are the primary, we commit the AOF. - // If we are the replica, we commit the AOF only if fast commit is disabled - // because we do not want to clobber AOF addresses. - // TODO: replica should instead wait until the next AOF commit is done via primary - if (serverOptions.EnableAOF) - { - if (serverOptions.EnableCluster && clusterProvider.IsReplica()) - { - if (!serverOptions.EnableFastCommit) - db.AppendOnlyFile?.CommitAsync().ConfigureAwait(false).GetAwaiter().GetResult(); - } - else - { - db.AppendOnlyFile?.CommitAsync().ConfigureAwait(false).GetAwaiter().GetResult(); - } - } - } - /// /// Commit AOF for all active databases /// @@ -975,9 +804,6 @@ public void Dispose() if (disposed) return; disposed = true; - // Wait for checkpoints to complete and disable checkpointing - checkpointTaskLock.WriteLock(); - itemBroker?.Dispose(); monitor?.Dispose(); ctsCommit?.Cancel(); @@ -987,53 +813,19 @@ public void Dispose() } /// - /// Mark the beginning of a checkpoint by taking and a lock to avoid concurrent checkpoint tasks + /// Mark the beginning of a checkpoint by taking and a lock to avoid concurrent checkpointing /// - /// - public bool TryPauseCheckpoints() - => checkpointTaskLock.TryWriteLock(); - - /// - /// Continuously try to take a lock for checkpointing until acquired or token was cancelled - /// - /// Cancellation token + /// ID of database to lock /// True if lock acquired - public async Task TryPauseCheckpointsContinuousAsync(CancellationToken token = default) - { - var checkpointsPaused = TryPauseCheckpoints(); - - while (!checkpointsPaused && !token.IsCancellationRequested && !disposed) - { - await Task.Yield(); - checkpointsPaused = TryPauseCheckpoints(); - } - - return checkpointsPaused; - } + public bool TryPauseCheckpoints(int dbId = 0) + => databaseManager.TryPauseCheckpoints(dbId); /// /// Release checkpoint task lock /// - public void ResumeCheckpoints() - => checkpointTaskLock.WriteUnlock(); - - /// - /// Continuously try to take a database read lock that ensures no db swap operations occur - /// - /// Cancellation token - /// True if lock acquired - public async Task TryGetDatabasesReadLockAsync(CancellationToken token = default) - { - var lockAcquired = databasesLock.TryReadLock(); - - while (!lockAcquired && !token.IsCancellationRequested && !disposed) - { - await Task.Yield(); - lockAcquired = databasesLock.TryReadLock(); - } - - return lockAcquired; - } + /// ID of database to unlock + public void ResumeCheckpoints(int dbId = 0) + => databaseManager.ResumeCheckpoints(dbId); /// /// Take a checkpoint if no checkpoint was taken after the provided time offset @@ -1041,258 +833,8 @@ public async Task TryGetDatabasesReadLockAsync(CancellationToken token = d /// /// /// - public async Task TakeOnDemandCheckpoint(DateTimeOffset entryTime, int dbId = 0) - { - // Take lock to ensure no other task will be taking a checkpoint - var checkpointsPaused = TryPauseCheckpoints(); - - // If an external task has taken a checkpoint beyond the provided entryTime return - if (!checkpointsPaused || databases.Map[dbId].LastSaveTime > entryTime) - { - if (checkpointsPaused) - ResumeCheckpoints(); - return; - } - - // Necessary to take a checkpoint because the latest checkpoint is before entryTime - await CheckpointTask(StoreType.All, dbId, checkpointsPaused: true, logger: logger); - } - - /// - /// Take checkpoint of all active databases - /// - /// - /// - /// - /// - /// - /// - public bool TakeCheckpoint(bool background, StoreType storeType = StoreType.All, int dbId = -1, ILogger logger = null, CancellationToken token = default) - { - var activeDbIdsSize = activeDbIdsLength; - - if (!allowMultiDb || activeDbIdsSize == 1 || dbId != -1) - { - if (dbId == -1) dbId = 0; - var checkpointTask = Task.Run(async () => await CheckpointTask(storeType, dbId, logger: logger, token: token), token); - if (background) - return true; - - checkpointTask.Wait(token); - return true; - } - - var lockAcquired = TryGetDatabasesReadLockAsync(token).Result; - if (!lockAcquired) return false; - - try - { - var activeDbIdsSnapshot = activeDbIds; - - var tasks = new Task[activeDbIdsSize]; - return CheckpointDatabases(storeType, ref activeDbIdsSnapshot, ref tasks, logger: logger, token: token); - } - finally - { - databasesLock.ReadUnlock(); - } - } - - /// - /// Asynchronously checkpoint a single database - /// - /// Store type to checkpoint - /// ID of database to checkpoint (default: DB 0) - /// True if lock previously acquired - /// Logger - /// Cancellation token - /// Task - private async Task CheckpointTask(StoreType storeType, int dbId = 0, bool checkpointsPaused = false, ILogger logger = null, CancellationToken token = default) - { - try - { - if (!checkpointsPaused) - { - // Take lock to ensure no other task will be taking a checkpoint - checkpointsPaused = await TryPauseCheckpointsContinuousAsync(token); - - if (!checkpointsPaused) return; - } - - await CheckpointDatabaseTask(dbId, storeType, logger); - } - catch (Exception ex) - { - logger?.LogError(ex, "Checkpointing threw exception"); - } - finally - { - if (checkpointsPaused) - ResumeCheckpoints(); - } - } - - /// - /// Asynchronously checkpoint multiple databases and wait for all to complete - /// - /// Store type to checkpoint - /// IDs of active databases to checkpoint - /// Optional tasks to use for asynchronous execution (must be the same size as dbIds) - /// Logger - /// Cancellation token - /// False if checkpointing already in progress - private bool CheckpointDatabases(StoreType storeType, ref int[] dbIds, ref Task[] tasks, ILogger logger = null, CancellationToken token = default) - { - try - { - Debug.Assert(tasks != null); - - // Prevent parallel checkpoint - if (!TryPauseCheckpoints()) return false; - - var currIdx = 0; - if (dbIds == null) - { - var activeDbIdsSize = activeDbIdsLength; - var activeDbIdsSnapshot = activeDbIds; - while (currIdx < activeDbIdsSize) - { - var dbId = activeDbIdsSnapshot[currIdx]; - tasks[currIdx] = CheckpointDatabaseTask(dbId, storeType, logger); - currIdx++; - } - } - else - { - while (currIdx < dbIds.Length && (currIdx == 0 || dbIds[currIdx] != 0)) - { - tasks[currIdx] = CheckpointDatabaseTask(dbIds[currIdx], storeType, logger); - currIdx++; - } - } - - for (var i = 0; i < currIdx; i++) - tasks[i].Wait(token); - } - catch (Exception ex) - { - logger?.LogError(ex, "Checkpointing threw exception"); - } - finally - { - ResumeCheckpoints(); - } - - return true; - } - - /// - /// Asynchronously checkpoint a single database - /// - /// ID of database to checkpoint - /// Store type to checkpoint - /// Logger - /// Task - private async Task CheckpointDatabaseTask(int dbId, StoreType storeType, ILogger logger) - { - var databasesMapSnapshot = databases.Map; - var db = databasesMapSnapshot[dbId]; - if (db.IsDefault()) return; - - DoCompaction(ref db); - var lastSaveStoreTailAddress = db.MainStore.Log.TailAddress; - var lastSaveObjectStoreTailAddress = (db.ObjectStore?.Log.TailAddress).GetValueOrDefault(); - - var full = db.LastSaveStoreTailAddress == 0 || - lastSaveStoreTailAddress - db.LastSaveStoreTailAddress >= serverOptions.FullCheckpointLogInterval || - (db.ObjectStore != null && (db.LastSaveObjectStoreTailAddress == 0 || - lastSaveObjectStoreTailAddress - db.LastSaveObjectStoreTailAddress >= serverOptions.FullCheckpointLogInterval)); - - var tryIncremental = serverOptions.EnableIncrementalSnapshots; - if (db.MainStore.IncrementalSnapshotTailAddress >= serverOptions.IncrementalSnapshotLogSizeLimit) - tryIncremental = false; - if (db.ObjectStore?.IncrementalSnapshotTailAddress >= serverOptions.IncrementalSnapshotLogSizeLimit) - tryIncremental = false; - - var checkpointType = serverOptions.UseFoldOverCheckpoints ? CheckpointType.FoldOver : CheckpointType.Snapshot; - await InitiateCheckpoint(dbId, db, full, checkpointType, tryIncremental, storeType, logger); - if (full) - { - if (storeType is StoreType.Main or StoreType.All) - db.LastSaveStoreTailAddress = lastSaveStoreTailAddress; - if (storeType is StoreType.Object or StoreType.All) - db.LastSaveObjectStoreTailAddress = lastSaveObjectStoreTailAddress; - } - - var lastSave = DateTimeOffset.UtcNow; - if (dbId == 0) - DefaultDatabase.LastSaveTime = lastSave; - db.LastSaveTime = lastSave; - databasesMapSnapshot[dbId] = db; - } - - private async Task InitiateCheckpoint(GarnetDatabase db, bool full, CheckpointType checkpointType, bool tryIncremental, StoreType storeType, ILogger logger = null) - { - logger?.LogInformation("Initiating checkpoint; full = {full}, type = {checkpointType}, tryIncremental = {tryIncremental}, storeType = {storeType}, dbId = {dbId}", full, checkpointType, tryIncremental, storeType, db.Id); - - long CheckpointCoveredAofAddress = 0; - if (db.AppendOnlyFile != null) - { - if (serverOptions.EnableCluster) - clusterProvider.OnCheckpointInitiated(out CheckpointCoveredAofAddress); - else - CheckpointCoveredAofAddress = db.AppendOnlyFile.TailAddress; - - if (CheckpointCoveredAofAddress > 0) - logger?.LogInformation("Will truncate AOF to {tailAddress} after checkpoint (files deleted after next commit)", CheckpointCoveredAofAddress); - } - - (bool success, Guid token) storeCheckpointResult = default; - (bool success, Guid token) objectStoreCheckpointResult = default; - if (full) - { - if (storeType is StoreType.Main or StoreType.All) - storeCheckpointResult = await db.MainStore.TakeFullCheckpointAsync(checkpointType); - - if (db.ObjectStore != null && (storeType == StoreType.Object || storeType == StoreType.All)) - objectStoreCheckpointResult = await db.ObjectStore.TakeFullCheckpointAsync(checkpointType); - } - else - { - if (storeType is StoreType.Main or StoreType.All) - storeCheckpointResult = await db.MainStore.TakeHybridLogCheckpointAsync(checkpointType, tryIncremental); - - if (db.ObjectStore != null && (storeType == StoreType.Object || storeType == StoreType.All)) - objectStoreCheckpointResult = await db.ObjectStore.TakeHybridLogCheckpointAsync(checkpointType, tryIncremental); - } - - // If cluster is enabled the replication manager is responsible for truncating AOF - if (serverOptions.EnableCluster && serverOptions.EnableAOF) - { - clusterProvider.SafeTruncateAOF(storeType, full, CheckpointCoveredAofAddress, storeCheckpointResult.token, objectStoreCheckpointResult.token); - } - else - { - db.AppendOnlyFile?.TruncateUntil(CheckpointCoveredAofAddress); - db.AppendOnlyFile?.Commit(); - } - - if (db.ObjectStore != null) - { - // During the checkpoint, we may have serialized Garnet objects in (v) versions of objects. - // We can now safely remove these serialized versions as they are no longer needed. - using (var iter1 = db.ObjectStore.Log.Scan(db.ObjectStore.Log.ReadOnlyAddress, db.ObjectStore.Log.TailAddress, ScanBufferingMode.SinglePageBuffering, includeSealedRecords: true)) - { - while (iter1.GetNext(out _, out _, out var value)) - { - if (value != null) - ((GarnetObjectBase)value).serialized = null; - } - } - } - - logger?.LogInformation("Completed checkpoint"); - } + public async Task TakeOnDemandCheckpoint(DateTimeOffset entryTime, int dbId = 0) => + await databaseManager.TakeOnDemandCheckpointAsync(entryTime, dbId); public bool HasKeysInSlots(List slots) { @@ -1314,7 +856,7 @@ public bool HasKeysInSlots(List slots) if (!hasKeyInSlots && objectStore != null) { - var functionsState = databaseManager.CreateFunctionsState(customCommandManager, GarnetObjectSerializer); + var functionsState = databaseManager.CreateFunctionsState(); var objstorefunctions = new ObjectSessionFunctions(functionsState); var objectStoreSession = objectStore?.NewSession(objstorefunctions); var iter = objectStoreSession.Iterate(); From b4d1e8001e344d2a21495bb9c70ca69c1539e630 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 27 Feb 2025 11:44:18 -0800 Subject: [PATCH 57/82] quit code change --- libs/server/Databases/DatabaseManagerBase.cs | 110 ++++++- libs/server/Databases/IDatabaseManager.cs | 41 +++ libs/server/Databases/MultiDatabaseManager.cs | 309 ++++++++++++++---- .../server/Databases/SingleDatabaseManager.cs | 55 +++- libs/server/StoreWrapper.cs | 288 ++-------------- 5 files changed, 458 insertions(+), 345 deletions(-) diff --git a/libs/server/Databases/DatabaseManagerBase.cs b/libs/server/Databases/DatabaseManagerBase.cs index fbd8a246b0..e362f8889e 100644 --- a/libs/server/Databases/DatabaseManagerBase.cs +++ b/libs/server/Databases/DatabaseManagerBase.cs @@ -78,15 +78,34 @@ public abstract bool TakeCheckpoint(bool background, int dbId, StoreType storeTy /// public abstract Task TakeOnDemandCheckpointAsync(DateTimeOffset entryTime, int dbId = 0); + /// public abstract Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLimit, CancellationToken token = default, ILogger logger = null); + /// + public abstract Task CommitToAofAsync(CancellationToken token = default, ILogger logger = null); + + /// + public abstract Task CommitToAofAsync(int dbId, CancellationToken token = default, ILogger logger = null); + + /// + public abstract Task WaitForCommitToAofAsync(CancellationToken token = default, ILogger logger = null); + /// public abstract void RecoverAOF(); /// public abstract long ReplayAOF(long untilAddress = -1); + /// + public abstract void DoCompaction(CancellationToken token = default); + + /// + public abstract bool GrowIndexesIfNeeded(CancellationToken token = default); + + /// + public abstract void StartObjectSizeTrackers(CancellationToken token = default); + /// public abstract void Reset(int dbId = 0); @@ -216,8 +235,6 @@ protected async Task InitiateCheckpointAsync(GarnetDatabase db, bool full, Check logger?.LogInformation("Completed checkpoint"); } - protected abstract ref GarnetDatabase GetDatabaseByRef(int dbId); - /// /// Asynchronously checkpoint a single database /// @@ -273,7 +290,8 @@ protected void RecoverDatabaseAOF(ref GarnetDatabase db) if (db.AppendOnlyFile == null) return; db.AppendOnlyFile.Recover(); - Logger?.LogInformation($"Recovered AOF: begin address = {db.AppendOnlyFile.BeginAddress}, tail address = {db.AppendOnlyFile.TailAddress}"); + Logger?.LogInformation("Recovered AOF: begin address = {beginAddress}, tail address = {tailAddress}", + db.AppendOnlyFile.BeginAddress, db.AppendOnlyFile.TailAddress); } protected long ReplayDatabaseAOF(AofProcessor aofProcessor, ref GarnetDatabase db, long untilAddress = -1) @@ -334,7 +352,7 @@ protected void FlushDatabase(ref GarnetDatabase db, bool unsafeTruncateLog) db.ObjectStore?.Log.ShiftBeginAddress(db.ObjectStore.Log.TailAddress, truncateLog: unsafeTruncateLog); } - private void DoCompaction(ref GarnetDatabase db) + protected void DoCompaction(ref GarnetDatabase db) { // Periodic compaction -> no need to compact before checkpointing if (StoreWrapper.serverOptions.CompactionFrequencySecs > 0) return; @@ -358,7 +376,8 @@ private void DoCompaction(ref GarnetDatabase db, int mainStoreMaxSegments, int o var compactLength = (1L << StoreWrapper.serverOptions.SegmentSizeBits()) * (mainStoreMaxSegments - numSegmentsToCompact); var untilAddress = readOnlyAddress - compactLength; Logger?.LogInformation( - $"Begin main store compact until {untilAddress}, Begin = {mainStoreLog.BeginAddress}, ReadOnly = {readOnlyAddress}, Tail = {mainStoreLog.TailAddress}"); + "Begin main store compact until {untilAddress}, Begin = {beginAddress}, ReadOnly = {readOnlyAddress}, Tail = {tailAddress}", + untilAddress, mainStoreLog.BeginAddress, readOnlyAddress, mainStoreLog.TailAddress); switch (compactionType) { @@ -386,7 +405,8 @@ private void DoCompaction(ref GarnetDatabase db, int mainStoreMaxSegments, int o } Logger?.LogInformation( - $"End main store compact until {untilAddress}, Begin = {mainStoreLog.BeginAddress}, ReadOnly = {readOnlyAddress}, Tail = {mainStoreLog.TailAddress}"); + "End main store compact until {untilAddress}, Begin = {beginAddress}, ReadOnly = {readOnlyAddress}, Tail = {tailAddress}", + untilAddress, mainStoreLog.BeginAddress, readOnlyAddress, mainStoreLog.TailAddress); } if (db.ObjectStore == null) return; @@ -401,7 +421,8 @@ private void DoCompaction(ref GarnetDatabase db, int mainStoreMaxSegments, int o var compactLength = (1L << StoreWrapper.serverOptions.ObjectStoreSegmentSizeBits()) * (objectStoreMaxSegments - numSegmentsToCompact); var untilAddress = readOnlyAddress - compactLength; Logger?.LogInformation( - $"Begin object store compact until {untilAddress}, Begin = {objectStoreLog.BeginAddress}, ReadOnly = {readOnlyAddress}, Tail = {objectStoreLog.TailAddress}"); + "Begin object store compact until {untilAddress}, Begin = {beginAddress}, ReadOnly = {readOnlyAddress}, Tail = {tailAddress}", + untilAddress, objectStoreLog.BeginAddress, readOnlyAddress, objectStoreLog.TailAddress); switch (compactionType) { @@ -431,7 +452,8 @@ private void DoCompaction(ref GarnetDatabase db, int mainStoreMaxSegments, int o } Logger?.LogInformation( - $"End object store compact until {untilAddress}, Begin = {mainStoreLog.BeginAddress}, ReadOnly = {readOnlyAddress}, Tail = {mainStoreLog.TailAddress}"); + "End object store compact until {untilAddress}, Begin = {beginAddress}, ReadOnly = {readOnlyAddress}, Tail = {tailAddress}", + untilAddress, mainStoreLog.BeginAddress, readOnlyAddress, mainStoreLog.TailAddress); } } @@ -454,5 +476,77 @@ private void CompactionCommitAof(ref GarnetDatabase db) } } } + + protected bool GrowIndexesIfNeeded(ref GarnetDatabase db) + { + var indexesMaxedOut = true; + + if (!DefaultDatabase.MainStoreIndexMaxedOut) + { + var dbMainStore = DefaultDatabase.MainStore; + if (GrowIndexIfNeeded(StoreType.Main, + StoreWrapper.serverOptions.AdjustedIndexMaxCacheLines, dbMainStore.OverflowBucketAllocations, + () => dbMainStore.IndexSize, () => dbMainStore.GrowIndex())) + { + db.MainStoreIndexMaxedOut = true; + } + else + { + indexesMaxedOut = false; + } + } + + if (!db.ObjectStoreIndexMaxedOut) + { + var dbObjectStore = db.ObjectStore; + if (GrowIndexIfNeeded(StoreType.Object, + StoreWrapper.serverOptions.AdjustedObjectStoreIndexMaxCacheLines, + dbObjectStore.OverflowBucketAllocations, + () => dbObjectStore.IndexSize, () => dbObjectStore.GrowIndex())) + { + db.ObjectStoreIndexMaxedOut = true; + } + else + { + indexesMaxedOut = false; + } + } + + return indexesMaxedOut; + } + + /// + /// Grows index if current size is smaller than max size. + /// Decision is based on whether overflow bucket allocation is more than a threshold which indicates a contention + /// in the index leading many allocations to the same bucket. + /// + /// + /// + /// + /// + /// + /// True if index has reached its max size + protected bool GrowIndexIfNeeded(StoreType storeType, long indexMaxSize, long overflowCount, Func indexSizeRetriever, Action growAction) + { + Logger?.LogDebug( + $"IndexAutoGrowTask[{{storeType}}]: checking index size {{indexSizeRetriever}} against max {{indexMaxSize}} with overflow {{overflowCount}}", + storeType, indexSizeRetriever(), indexMaxSize, overflowCount); + + if (indexSizeRetriever() < indexMaxSize && + overflowCount > (indexSizeRetriever() * StoreWrapper.serverOptions.IndexResizeThreshold / 100)) + { + Logger?.LogInformation( + $"IndexAutoGrowTask[{{storeType}}]: overflowCount {{overflowCount}} ratio more than threshold {{indexResizeThreshold}}%. Doubling index size...", + storeType, overflowCount, StoreWrapper.serverOptions.IndexResizeThreshold); + growAction(); + } + + if (indexSizeRetriever() < indexMaxSize) return false; + + Logger?.LogDebug( + $"IndexAutoGrowTask[{{storeType}}]: checking index size {{indexSizeRetriever}} against max {{indexMaxSize}} with overflow {{overflowCount}}", + storeType, indexSizeRetriever(), indexMaxSize, overflowCount); + return true; + } } } diff --git a/libs/server/Databases/IDatabaseManager.cs b/libs/server/Databases/IDatabaseManager.cs index cf6ba4b471..a0196b0cc0 100644 --- a/libs/server/Databases/IDatabaseManager.cs +++ b/libs/server/Databases/IDatabaseManager.cs @@ -120,6 +120,31 @@ public bool TakeCheckpoint(bool background, int dbId, StoreType storeType = Stor public Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLimit, CancellationToken token = default, ILogger logger = null); + /// + /// Commit to AOF for all active databases + /// + /// Cancellation token + /// Logger + /// Task + public Task CommitToAofAsync(CancellationToken token = default, ILogger logger = null); + + /// + /// Commit to AOF for specified database + /// + /// ID of database to commit + /// Cancellation token + /// Logger + /// Task + public Task CommitToAofAsync(int dbId, CancellationToken token = default, ILogger logger = null); + + /// + /// Wait for commit to AOF for all active databases + /// + /// Cancellation token + /// Logger + /// Task + public Task WaitForCommitToAofAsync(CancellationToken token = default, ILogger logger = null); + /// /// Recover AOF /// @@ -130,6 +155,22 @@ public Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLimit, Cancellati /// public long ReplayAOF(long untilAddress = -1); + /// + /// Do compaction + /// + public void DoCompaction(CancellationToken token = default); + + /// + /// Grows indexes of both main store and object store for all active databases if current size is too small + /// + /// True if indexes are maxed out + public bool GrowIndexesIfNeeded(CancellationToken token = default); + + /// + /// Start object size trackers for all active databases + /// + public void StartObjectSizeTrackers(CancellationToken token = default); + /// /// Reset /// diff --git a/libs/server/Databases/MultiDatabaseManager.cs b/libs/server/Databases/MultiDatabaseManager.cs index a416e7d1f6..3690fb7075 100644 --- a/libs/server/Databases/MultiDatabaseManager.cs +++ b/libs/server/Databases/MultiDatabaseManager.cs @@ -43,13 +43,7 @@ internal class MultiDatabaseManager : DatabaseManagerBase Task[] checkpointTasks; // Reusable array for storing database IDs for checkpointing - int[] dbIdsToCheckpoint = null; - - // Reusable task array for tracking aof commits of multiple DBs - // Used by recurring aof commits task if multiple DBs exist - Task[] aofTasks; - - readonly object activeDbIdsLock = new(); + int[] dbIdsToCheckpoint; // Path of serialization for the DB IDs file used when committing / recovering to / from AOF readonly string aofParentDir; @@ -64,11 +58,11 @@ internal class MultiDatabaseManager : DatabaseManagerBase public MultiDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabaseDelegate, StoreWrapper storeWrapper, bool createDefaultDatabase = true) : base(createsDatabaseDelegate, storeWrapper) { - this.maxDatabases = storeWrapper.serverOptions.MaxDatabases; - this.Logger = storeWrapper.loggerFactory?.CreateLogger(nameof(MultiDatabaseManager)); + maxDatabases = storeWrapper.serverOptions.MaxDatabases; + Logger = storeWrapper.loggerFactory?.CreateLogger(nameof(MultiDatabaseManager)); // Create default databases map of size 1 - databases = new ExpandableMap(1, 0, this.maxDatabases - 1); + databases = new ExpandableMap(1, 0, maxDatabases - 1); // Create default database of index 0 (unless specified otherwise) if (createDefaultDatabase) @@ -76,18 +70,18 @@ public MultiDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabase var db = CreateDatabaseDelegate(0, out var storeCheckpointDir, out var aofDir); var checkpointDirInfo = new DirectoryInfo(storeCheckpointDir); - this.checkpointDirBaseName = checkpointDirInfo.Name; - this.checkpointParentDir = checkpointDirInfo.Parent!.FullName; + checkpointDirBaseName = checkpointDirInfo.Name; + checkpointParentDir = checkpointDirInfo.Parent!.FullName; if (aofDir != null) { var aofDirInfo = new DirectoryInfo(aofDir); - this.aofDirBaseName = aofDirInfo.Name; - this.aofParentDir = aofDirInfo.Parent!.FullName; + aofDirBaseName = aofDirInfo.Name; + aofParentDir = aofDirInfo.Parent!.FullName; } // Set new database in map - if (!this.TryAddDatabase(0, ref db)) + if (!TryAddDatabase(0, ref db)) throw new GarnetException("Failed to set initial database in databases map"); } } @@ -95,7 +89,7 @@ public MultiDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabase public MultiDatabaseManager(MultiDatabaseManager src, bool enableAof) : this(src.CreateDatabaseDelegate, src.StoreWrapper, createDefaultDatabase: false) { - this.CopyDatabases(src, enableAof); + CopyDatabases(src, enableAof); } /// @@ -114,7 +108,9 @@ public override void RecoverCheckpoint(bool replicaRecover = false, bool recover } catch (Exception ex) { - Logger?.LogInformation(ex, $"Error during recovery of database ids; checkpointParentDir = {checkpointParentDir}; checkpointDirBaseName = {checkpointDirBaseName}"); + Logger?.LogInformation(ex, + "Error during recovery of database ids; checkpointParentDir = {checkpointParentDir}; checkpointDirBaseName = {checkpointDirBaseName}", + checkpointParentDir, checkpointDirBaseName); if (StoreWrapper.serverOptions.FailOnRecoveryError) throw; return; @@ -134,11 +130,15 @@ public override void RecoverCheckpoint(bool replicaRecover = false, bool recover catch (TsavoriteNoHybridLogException ex) { // No hybrid log being found is not the same as an error in recovery. e.g. fresh start - Logger?.LogInformation(ex, $"No Hybrid Log found for recovery; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}", storeVersion, objectStoreVersion); + Logger?.LogInformation(ex, + "No Hybrid Log found for recovery; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}", + storeVersion, objectStoreVersion); } catch (Exception ex) { - Logger?.LogInformation(ex, $"Error during recovery of store; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}", storeVersion, objectStoreVersion); + Logger?.LogInformation(ex, + "Error during recovery of store; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}", + storeVersion, objectStoreVersion); if (StoreWrapper.serverOptions.FailOnRecoveryError) throw; } @@ -181,7 +181,7 @@ public override bool TakeCheckpoint(bool background, int dbId, StoreType storeTy var checkpointTask = TakeCheckpointAsync(databasesMapSnapshot[dbId], storeType, logger: logger, token: token).ContinueWith( _ => { - databasesMapSnapshot[dbId].LastSaveTime = DateTimeOffset.UtcNow; + TryUpdateLastSaveTimeAsync(dbId, token).GetAwaiter().GetResult(); ResumeCheckpoints(dbId); }, TaskContinuationOptions.ExecuteSynchronously).GetAwaiter(); @@ -211,7 +211,7 @@ public override async Task TakeOnDemandCheckpointAsync(DateTimeOffset entryTime, // Necessary to take a checkpoint because the latest checkpoint is before entryTime await TakeCheckpointAsync(databasesMapSnapshot[dbId], StoreType.All, logger: Logger); - databasesMapSnapshot[dbId].LastSaveTime = DateTimeOffset.UtcNow; + TryUpdateLastSaveTimeAsync(dbId).GetAwaiter().GetResult(); } finally { @@ -219,6 +219,7 @@ public override async Task TakeOnDemandCheckpointAsync(DateTimeOffset entryTime, } } + /// public override async Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLimit, CancellationToken token = default, ILogger logger = null) { @@ -241,7 +242,8 @@ public override async Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLi var dbAofSize = db.AppendOnlyFile.TailAddress - db.AppendOnlyFile.BeginAddress; if (dbAofSize > aofSizeLimit) { - logger?.LogInformation($"Enforcing AOF size limit currentAofSize: {dbAofSize} > AofSizeLimit: {aofSizeLimit} (Database ID: {dbId})"); + logger?.LogInformation("Enforcing AOF size limit currentAofSize: {dbAofSize} > AofSizeLimit: {aofSizeLimit} (Database ID: {dbId})", + dbAofSize, aofSizeLimit, dbId); dbIdsToCheckpoint[dbIdsIdx++] = dbId; break; } @@ -257,6 +259,82 @@ public override async Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLi } } + /// + public override async Task CommitToAofAsync(CancellationToken token = default, ILogger logger = null) + { + var databasesMapSnapshot = databases.Map; + + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + + // Take a read lock to make sure that swap-db operation is not in progress + var lockAcquired = TryGetDatabasesReadLockAsync(token).Result; + if (!lockAcquired) return; + + var aofTasks = new Task[activeDbIdsSize]; + + try + { + for (var i = 0; i < activeDbIdsSize; i++) + { + var dbId = activeDbIdsSnapshot[i]; + var db = databasesMapSnapshot[dbId]; + Debug.Assert(!db.IsDefault()); + + aofTasks[i] = db.AppendOnlyFile.CommitAsync(token: token).AsTask(); + } + + await Task.WhenAll(aofTasks); + } + finally + { + databasesLock.ReadUnlock(); + } + } + + /// + public override async Task CommitToAofAsync(int dbId, CancellationToken token = default, ILogger logger = null) + { + var databasesMapSize = databases.ActualSize; + var databasesMapSnapshot = databases.Map; + Debug.Assert(dbId < databasesMapSize && !databasesMapSnapshot[dbId].IsDefault()); + + await databasesMapSnapshot[dbId].AppendOnlyFile.CommitAsync(token: token); + } + + /// + public override async Task WaitForCommitToAofAsync(CancellationToken token = default, ILogger logger = null) + { + // Take a read lock to make sure that swap-db operation is not in progress + var lockAcquired = TryGetDatabasesReadLockAsync(token).Result; + if (!lockAcquired) return; + + try + { + var databasesMapSnapshot = databases.Map; + + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + + var aofTasks = new Task[activeDbIdsSize]; + + for (var i = 0; i < activeDbIdsSize; i++) + { + var dbId = activeDbIdsSnapshot[i]; + var db = databasesMapSnapshot[dbId]; + Debug.Assert(!db.IsDefault()); + + aofTasks[i] = db.AppendOnlyFile.WaitForCommitAsync(token: token).AsTask(); + } + + await Task.WhenAll(aofTasks); + } + finally + { + databasesLock.ReadUnlock(); + } + } + /// public override void RecoverAOF() { @@ -269,7 +347,9 @@ public override void RecoverAOF() } catch (Exception ex) { - Logger?.LogInformation(ex, $"Error during recovery of database ids; aofParentDir = {aofParentDir}; aofDirBaseName = {aofDirBaseName}"); + Logger?.LogInformation(ex, + "Error during recovery of database ids; aofParentDir = {aofParentDir}; aofDirBaseName = {aofDirBaseName}", + aofParentDir, aofDirBaseName); return; } @@ -315,10 +395,98 @@ public override long ReplayAOF(long untilAddress = -1) return replicationOffset; } + /// + public override void DoCompaction(CancellationToken token = default) + { + var lockAcquired = TryGetDatabasesReadLockAsync(token).Result; + if (!lockAcquired) return; + + try + { + var databasesMapSnapshot = databases.Map; + + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + + for (var i = 0; i < activeDbIdsSize; i++) + { + var dbId = activeDbIdsSnapshot[i]; + var db = databasesMapSnapshot[dbId]; + Debug.Assert(!db.IsDefault()); + + DoCompaction(ref db); + } + } + finally + { + databasesLock.ReadUnlock(); + } + } + + /// + public override bool GrowIndexesIfNeeded(CancellationToken token = default) + { + var allIndexesMaxedOut = true; + + var lockAcquired = TryGetDatabasesReadLockAsync(token).Result; + if (!lockAcquired) return false; + + try + { + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + + var databasesMapSnapshot = databases.Map; + + for (var i = 0; i < activeDbIdsSize; i++) + { + var dbId = activeDbIdsSnapshot[i]; + + var indexesMaxedOut = GrowIndexesIfNeeded(ref databasesMapSnapshot[dbId]); + if (allIndexesMaxedOut && !indexesMaxedOut) + allIndexesMaxedOut = false; + } + + return allIndexesMaxedOut; + } + finally + { + databasesLock.ReadUnlock(); + } + } + + /// + public override void StartObjectSizeTrackers(CancellationToken token = default) + { + var lockAcquired = TryGetDatabasesReadLockAsync(token).Result; + if (!lockAcquired) return; + + try + { + var databasesMapSnapshot = databases.Map; + + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + + for (var i = 0; i < activeDbIdsSize; i++) + { + var dbId = activeDbIdsSnapshot[i]; + var db = databasesMapSnapshot[dbId]; + Debug.Assert(!db.IsDefault()); + + db.ObjectStoreSizeTracker?.Start(token); + } + } + finally + { + databasesLock.ReadUnlock(); + } + } + /// public override void Reset(int dbId = 0) { - if (!this.TryGetOrAddDatabase(dbId, out var db)) + if (!TryGetOrAddDatabase(dbId, out var db)) throw new GarnetException($"Database with ID {dbId} was not found."); ResetDatabase(ref db); @@ -326,7 +494,7 @@ public override void Reset(int dbId = 0) public override void EnqueueCommit(bool isMainStore, long version, int dbId = 0) { - if (!this.TryGetOrAddDatabase(dbId, out var db)) + if (!TryGetOrAddDatabase(dbId, out var db)) throw new GarnetException($"Database with ID {dbId} was not found."); EnqueueDatabaseCommit(ref db, isMainStore, version); @@ -335,14 +503,14 @@ public override void EnqueueCommit(bool isMainStore, long version, int dbId = 0) /// public override bool TrySwapDatabases(int dbId1, int dbId2) { - if (!this.TryGetOrAddDatabase(dbId1, out var db1) || - !this.TryGetOrAddDatabase(dbId2, out var db2)) + if (!TryGetOrAddDatabase(dbId1, out var db1) || + !TryGetOrAddDatabase(dbId2, out var db2)) return false; databasesLock.WriteLock(); try { - var databaseMapSnapshot = this.databases.Map; + var databaseMapSnapshot = databases.Map; databaseMapSnapshot[dbId2] = db1; databaseMapSnapshot[dbId1] = db2; } @@ -359,22 +527,13 @@ public override bool TrySwapDatabases(int dbId1, int dbId2) public override FunctionsState CreateFunctionsState(int dbId = 0) { - if (!this.TryGetOrAddDatabase(dbId, out var db)) + if (!TryGetOrAddDatabase(dbId, out var db)) throw new GarnetException($"Database with ID {dbId} was not found."); return new(db.AppendOnlyFile, db.VersionMap, StoreWrapper.customCommandManager, null, db.ObjectStoreSizeTracker, StoreWrapper.GarnetObjectSerializer); } - protected override ref GarnetDatabase GetDatabaseByRef(int dbId) - { - var databasesMapSize = databases.ActualSize; - var databasesMapSnapshot = databases.Map; - - Debug.Assert(dbId < databasesMapSize); - return ref databasesMapSnapshot[dbId]; - } - /// public override bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db) { @@ -388,19 +547,18 @@ public override bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db) } /// - public override bool TryPauseCheckpoints(int dbId = 0) + public override bool TryPauseCheckpoints(int dbId) { - if (!this.TryGetOrAddDatabase(dbId, out var db)) + if (!TryGetOrAddDatabase(dbId, out var db)) throw new GarnetException($"Database with ID {dbId} was not found."); return TryPauseCheckpoints(ref db); } /// - public override async Task TryPauseCheckpointsContinuousAsync(int dbId = 0, - CancellationToken token = default) + public override async Task TryPauseCheckpointsContinuousAsync(int dbId, CancellationToken token = default) { - if (!this.TryGetOrAddDatabase(dbId, out var db)) + if (!TryGetOrAddDatabase(dbId, out var db)) throw new GarnetException($"Database with ID {dbId} was not found."); var checkpointsPaused = TryPauseCheckpoints(ref db); @@ -415,9 +573,9 @@ public override async Task TryPauseCheckpointsContinuousAsync(int dbId = 0 } /// - public override void ResumeCheckpoints(int dbId = 0) + public override void ResumeCheckpoints(int dbId) { - if (!this.TryGetOrAddDatabase(dbId, out var db)) + if (!TryGetOrAddDatabase(dbId, out var db)) throw new GarnetException($"Database with ID {dbId} was not found."); ResumeCheckpoints(ref db); @@ -444,13 +602,13 @@ public override bool TryGetDatabase(int dbId, out GarnetDatabase db) } // Try to retrieve or add database - return this.TryGetOrAddDatabase(dbId, out db); + return TryGetOrAddDatabase(dbId, out db); } /// public override void FlushDatabase(bool unsafeTruncateLog, int dbId = 0) { - if (!this.TryGetOrAddDatabase(dbId, out var db)) + if (!TryGetOrAddDatabase(dbId, out var db)) throw new GarnetException($"Database with ID {dbId} was not found."); FlushDatabase(ref db, unsafeTruncateLog); @@ -466,12 +624,12 @@ public override void FlushAllDatabases(bool unsafeTruncateLog) for (var i = 0; i < activeDbIdsSize; i++) { var dbId = activeDbIdsSnapshot[i]; - this.FlushDatabase(ref databaseMapSnapshot[dbId], unsafeTruncateLog); + FlushDatabase(ref databaseMapSnapshot[dbId], unsafeTruncateLog); } } /// - /// Continuously try to take a database read lock that ensures no db swap operations occur + /// Continuously try to take a database read lock /// /// Cancellation token /// True if lock acquired @@ -488,6 +646,24 @@ public async Task TryGetDatabasesReadLockAsync(CancellationToken token = d return lockAcquired; } + /// + /// Continuously try to take a database write lock + /// + /// Cancellation token + /// True if lock acquired + public async Task TryGetDatabasesWriteLockAsync(CancellationToken token = default) + { + var lockAcquired = databasesLock.TryWriteLock(); + + while (!lockAcquired && !token.IsCancellationRequested && !Disposed) + { + await Task.Yield(); + lockAcquired = databasesLock.TryWriteLock(); + } + + return lockAcquired; + } + /// /// Try to add a new database /// @@ -525,7 +701,9 @@ private void HandleDatabaseAdded(int dbId) return; } - lock (activeDbIdsLock) + if (!TryGetDatabasesWriteLockAsync().GetAwaiter().GetResult()) return; + + try { // Select the next size of activeDbIds (as multiple of 2 from the existing size) var newSize = activeDbIds?.Length ?? 1; @@ -549,9 +727,12 @@ private void HandleDatabaseAdded(int dbId) activeDbIds = activeDbIdsUpdated; activeDbIdsLength = dbIdIdx + 1; checkpointTasks = new Task[activeDbIdsLength]; - aofTasks = new Task[activeDbIdsLength]; dbIdsToCheckpoint = new int[activeDbIdsLength]; } + finally + { + databasesLock.WriteUnlock(); + } } /// @@ -596,7 +777,7 @@ private void CopyDatabases(IDatabaseManager src, bool enableAof) { case SingleDatabaseManager sdbm: var defaultDbCopy = new GarnetDatabase(ref sdbm.DefaultDatabase, enableAof); - this.TryAddDatabase(0, ref defaultDbCopy); + TryAddDatabase(0, ref defaultDbCopy); return; case MultiDatabaseManager mdbm: var activeDbIdsSize = mdbm.activeDbIdsLength; @@ -607,7 +788,7 @@ private void CopyDatabases(IDatabaseManager src, bool enableAof) { var dbId = activeDbIdsSnapshot[i]; var dbCopy = new GarnetDatabase(ref databasesMapSnapshot[dbId], enableAof); - this.TryAddDatabase(dbId, ref dbCopy); + TryAddDatabase(dbId, ref dbCopy); } return; @@ -647,7 +828,7 @@ private async Task TakeDatabasesCheckpointAsync(StoreType storeType, int dbIdsCo checkpointTasks[currIdx] = TakeCheckpointAsync(databaseMapSnapshot[dbId], storeType, logger: logger, token: token).ContinueWith( _ => { - databaseMapSnapshot[dbId].LastSaveTime = DateTimeOffset.UtcNow; + TryUpdateLastSaveTimeAsync(dbId, token).GetAwaiter().GetResult(); ResumeCheckpoints(dbId); }, TaskContinuationOptions.ExecuteSynchronously); @@ -660,7 +841,25 @@ private async Task TakeDatabasesCheckpointAsync(StoreType storeType, int dbIdsCo } catch (Exception ex) { - logger?.LogError(ex, $"Checkpointing threw exception"); + logger?.LogError(ex, "Checkpointing threw exception"); + } + } + + private async Task TryUpdateLastSaveTimeAsync(int dbId, CancellationToken token = default) + { + var lockAcquired = await TryGetDatabasesReadLockAsync(token); + if (!lockAcquired) return false; + + var databasesMapSnapshot = databases.Map; + + try + { + databasesMapSnapshot[dbId].LastSaveTime = DateTimeOffset.UtcNow; + return true; + } + finally + { + databasesLock.ReadUnlock(); } } diff --git a/libs/server/Databases/SingleDatabaseManager.cs b/libs/server/Databases/SingleDatabaseManager.cs index 7790269f7a..64ef1e3743 100644 --- a/libs/server/Databases/SingleDatabaseManager.cs +++ b/libs/server/Databases/SingleDatabaseManager.cs @@ -22,7 +22,7 @@ internal class SingleDatabaseManager : DatabaseManagerBase public SingleDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabaseDelegate, StoreWrapper storeWrapper, bool createDefaultDatabase = true) : base(createsDatabaseDelegate, storeWrapper) { - this.Logger = storeWrapper.loggerFactory?.CreateLogger(nameof(SingleDatabaseManager)); + Logger = storeWrapper.loggerFactory?.CreateLogger(nameof(SingleDatabaseManager)); // Create default database of index 0 (unless specified otherwise) if (createDefaultDatabase) @@ -33,7 +33,7 @@ public SingleDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabas public SingleDatabaseManager(SingleDatabaseManager src, bool enableAof) : this(src.CreateDatabaseDelegate, src.StoreWrapper, createDefaultDatabase: false) { - this.CopyDatabases(src, enableAof); + CopyDatabases(src, enableAof); } /// @@ -77,11 +77,15 @@ public override void RecoverCheckpoint(bool replicaRecover = false, bool recover catch (TsavoriteNoHybridLogException ex) { // No hybrid log being found is not the same as an error in recovery. e.g. fresh start - Logger?.LogInformation(ex, $"No Hybrid Log found for recovery; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}"); + Logger?.LogInformation(ex, + "No Hybrid Log found for recovery; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}", + storeVersion, objectStoreVersion); } catch (Exception ex) { - Logger?.LogInformation(ex, $"Error during recovery of store; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}"); + Logger?.LogInformation(ex, + "Error during recovery of store; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}", + storeVersion, objectStoreVersion); if (StoreWrapper.serverOptions.FailOnRecoveryError) throw; @@ -187,7 +191,8 @@ public override async Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLi if (!TryPauseCheckpointsContinuousAsync(DefaultDatabase.Id, token: token).GetAwaiter().GetResult()) return; - logger?.LogInformation($"Enforcing AOF size limit currentAofSize: {aofSize} > AofSizeLimit: {aofSizeLimit}"); + logger?.LogInformation("Enforcing AOF size limit currentAofSize: {aofSize} > AofSizeLimit: {aofSizeLimit}", + aofSize, aofSizeLimit); try { @@ -201,6 +206,25 @@ public override async Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLi } } + /// + public override async Task CommitToAofAsync(CancellationToken token = default, ILogger logger = null) + { + await AppendOnlyFile.CommitAsync(token: token); + } + + /// + public override async Task CommitToAofAsync(int dbId, CancellationToken token = default, ILogger logger = null) + { + ArgumentOutOfRangeException.ThrowIfNotEqual(dbId, 0); + + await CommitToAofAsync(token, logger); + } + + public override async Task WaitForCommitToAofAsync(CancellationToken token = default, ILogger logger = null) + { + await AppendOnlyFile.WaitForCommitAsync(token: token); + } + /// public override void RecoverAOF() => RecoverDatabaseAOF(ref DefaultDatabase); @@ -223,7 +247,17 @@ public override long ReplayAOF(long untilAddress = -1) aofProcessor.Dispose(); } } - + + /// + public override void DoCompaction(CancellationToken token = default) => DoCompaction(ref DefaultDatabase); + + public override bool GrowIndexesIfNeeded(CancellationToken token = default) => + GrowIndexesIfNeeded(ref DefaultDatabase); + + /// + public override void StartObjectSizeTrackers(CancellationToken token = default) => + ObjectStoreSizeTracker?.Start(token); + /// public override void Reset(int dbId = 0) { @@ -275,16 +309,9 @@ public override FunctionsState CreateFunctionsState(int dbId = 0) StoreWrapper.GarnetObjectSerializer); } - protected override ref GarnetDatabase GetDatabaseByRef(int dbId) - { - ArgumentOutOfRangeException.ThrowIfNotEqual(dbId, 0); - - return ref DefaultDatabase; - } - private void CopyDatabases(SingleDatabaseManager src, bool enableAof) { - this.DefaultDatabase = new GarnetDatabase(ref src.DefaultDatabase, enableAof); + DefaultDatabase = new GarnetDatabase(ref src.DefaultDatabase, enableAof); } public override void Dispose() diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index a2b027e79a..2fe8df3dce 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -63,11 +63,6 @@ public sealed class StoreWrapper /// public CacheSizeTracker objectStoreSizeTracker => databaseManager.ObjectStoreSizeTracker; - /// - /// Version map (of DB 0) - /// - internal WatchVersionMap versionMap => databaseManager.VersionMap; - /// /// Server options /// @@ -133,11 +128,8 @@ public sealed class StoreWrapper readonly CancellationTokenSource ctsCommit; - // True if this server supports more than one logical database - readonly bool allowMultiDb; - // True if StoreWrapper instance is disposed - bool disposed = false; + bool disposed; /// /// Constructor @@ -176,7 +168,6 @@ public StoreWrapper( this.loggingFrequency = TimeSpan.FromSeconds(serverOptions.LoggingFrequency); // If cluster mode is off and more than one database allowed multi-db mode is turned on - this.allowMultiDb = !this.serverOptions.EnableCluster && this.serverOptions.MaxDatabases > 1; if (serverOptions.SlowLogThreshold > 0) this.slowLogContainer = new SlowLogContainer(serverOptions.SlowLogMaxEntries); @@ -197,7 +188,7 @@ public StoreWrapper( typeof(AclAuthenticationSettings))) { // Create a new access control list and register it with the authentication settings - AclAuthenticationSettings aclAuthenticationSettings = + var aclAuthenticationSettings = (AclAuthenticationSettings)this.serverOptions.AuthSettings; if (!string.IsNullOrEmpty(aclAuthenticationSettings.AclConfigurationFile)) @@ -295,6 +286,9 @@ internal void Recover() } } + /// + /// Recover checkpoint + /// public void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStoreFromToken = false, bool recoverObjectStoreFromToken = false, CheckpointMetadata metadata = null) => databaseManager.RecoverCheckpoint(replicaRecover, recoverMainStoreFromToken, recoverObjectStoreFromToken, metadata); @@ -365,16 +359,7 @@ async Task CommitTask(int commitFrequencyMs, ILogger logger = null, Cancellation } else { - var activeDbIdsSize = this.activeDbIdsLength; - - if (!allowMultiDb || activeDbIdsSize == 1) - { - await appendOnlyFile.CommitAsync(null, token); - } - else - { - MultiDatabaseCommit(ref aofTasks, token); - } + await databaseManager.CommitToAofAsync(token, logger); await Task.Delay(commitFrequencyMs, token); } @@ -386,42 +371,6 @@ async Task CommitTask(int commitFrequencyMs, ILogger logger = null, Cancellation } } - /// - /// Perform AOF commits on all active databases - /// - /// Optional reference to pre-allocated array of tasks to use - /// Cancellation token - /// True if should wait until all tasks complete - void MultiDatabaseCommit(ref Task[] tasks, CancellationToken token, bool spinWait = true) - { - var databasesMapSnapshot = databases.Map; - - var activeDbIdsSize = activeDbIdsLength; - var activeDbIdsSnapshot = activeDbIds; - - tasks ??= new Task[activeDbIdsSize]; - - // Take a read lock to make sure that swap-db operation is not in progress - var lockAcquired = TryGetDatabasesReadLockAsync(token).Result; - if (!lockAcquired) return; - - for (var i = 0; i < activeDbIdsSize; i++) - { - var dbId = activeDbIdsSnapshot[i]; - var db = databasesMapSnapshot[dbId]; - Debug.Assert(!db.IsDefault()); - - tasks[i] = db.AppendOnlyFile.CommitAsync(null, token).AsTask(); - } - - var completion = Task.WhenAll(tasks).ContinueWith(_ => databasesLock.ReadUnlock(), token); - - if (!spinWait) - return; - - completion.Wait(token); - } - async Task CompactionTask(int compactionFrequencySecs, CancellationToken token = default) { Debug.Assert(compactionFrequencySecs > 0); @@ -431,29 +380,7 @@ async Task CompactionTask(int compactionFrequencySecs, CancellationToken token = { if (token.IsCancellationRequested) return; - var lockAcquired = TryGetDatabasesReadLockAsync(token).Result; - if (!lockAcquired) continue; - - try - { - var databasesMapSnapshot = databases.Map; - - var activeDbIdsSize = activeDbIdsLength; - var activeDbIdsSnapshot = activeDbIds; - - for (var i = 0; i < activeDbIdsSize; i++) - { - var dbId = activeDbIdsSnapshot[i]; - var db = databasesMapSnapshot[dbId]; - Debug.Assert(!db.IsDefault()); - - DoCompaction(ref db, serverOptions.CompactionMaxSegments, serverOptions.ObjectStoreCompactionMaxSegments, 1, serverOptions.CompactionType, serverOptions.CompactionForceDelete); - } - } - finally - { - databasesLock.ReadUnlock(); - } + databaseManager.DoCompaction(token); if (!serverOptions.CompactionForceDelete) logger?.LogInformation("NOTE: Take a checkpoint (SAVE/BGSAVE) in order to actually delete the older data segments (files) from disk"); @@ -520,49 +447,17 @@ internal void CommitAOF(bool spinWait) { if (!serverOptions.EnableAOF || appendOnlyFile == null) return; - var activeDbIdsSize = this.activeDbIdsLength; - if (!allowMultiDb || activeDbIdsSize == 1) - { - appendOnlyFile.Commit(spinWait); - return; - } + var task = databaseManager.CommitToAofAsync(); + if (!spinWait) return; - Task[] tasks = null; - MultiDatabaseCommit(ref tasks, CancellationToken.None, spinWait); + task.GetAwaiter().GetResult(); } /// /// Wait for commits from all active databases /// - /// - internal void WaitForCommit() - { - if (!serverOptions.EnableAOF || appendOnlyFile == null) return; - - var activeDbIdsSize = this.activeDbIdsLength; - if (!allowMultiDb || activeDbIdsSize == 1) - { - appendOnlyFile.WaitForCommit(); - return; - } - - // Take a read lock to make sure that swap-db operation is not in progress - var lockAcquired = TryGetDatabasesReadLockAsync().Result; - if (!lockAcquired) return; - - var databasesMapSnapshot = databases.Map; - var activeDbIdsSnapshot = activeDbIds; - - var tasks = new Task[activeDbIdsSize]; - for (var i = 0; i < activeDbIdsSize; i++) - { - var dbId = activeDbIdsSnapshot[i]; - var db = databasesMapSnapshot[dbId]; - tasks[i] = db.AppendOnlyFile.WaitForCommitAsync().AsTask(); - } - - Task.WhenAll(tasks).ContinueWith(_ => databasesLock.ReadUnlock()).Wait(); - } + internal void WaitForCommit() => + WaitForCommitAsync().GetAwaiter().GetResult(); /// /// Asynchronously wait for commits from all active databases @@ -573,29 +468,7 @@ internal async ValueTask WaitForCommitAsync(CancellationToken token = default) { if (!serverOptions.EnableAOF || appendOnlyFile == null) return; - var activeDbIdsSize = this.activeDbIdsLength; - if (!allowMultiDb || activeDbIdsSize == 1) - { - await appendOnlyFile.WaitForCommitAsync(token: token); - return; - } - - // Take a read lock to make sure that swap-db operation is not in progress - var lockAcquired = TryGetDatabasesReadLockAsync(token).Result; - if (!lockAcquired) return; - - var databasesMapSnapshot = databases.Map; - var activeDbIdsSnapshot = activeDbIds; - - var tasks = new Task[activeDbIdsSize]; - for (var i = 0; i < activeDbIdsSize; i++) - { - var dbId = activeDbIdsSnapshot[i]; - var db = databasesMapSnapshot[dbId]; - tasks[i] = db.AppendOnlyFile.WaitForCommitAsync(token: token).AsTask(); - } - - await Task.WhenAll(tasks).ContinueWith(_ => databasesLock.ReadUnlock(), token); + await databaseManager.WaitForCommitToAofAsync(token); } /// @@ -609,31 +482,13 @@ internal async ValueTask CommitAOFAsync(int dbId = -1, CancellationToken token = { if (!serverOptions.EnableAOF || appendOnlyFile == null) return; - var activeDbIdsSize = this.activeDbIdsLength; - if (!allowMultiDb || activeDbIdsSize == 1 || dbId != -1) + if (dbId == -1) { - if (dbId == -1) dbId = 0; - var dbFound = TryGetDatabase(dbId, out var db); - Debug.Assert(dbFound); - await db.AppendOnlyFile.CommitAsync(token: token); + await databaseManager.CommitToAofAsync(token, logger); return; } - var lockAcquired = TryGetDatabasesReadLockAsync(token).Result; - if (!lockAcquired) return; - - var databasesMapSnapshot = databases.Map; - var activeDbIdsSnapshot = activeDbIds; - - var tasks = new Task[activeDbIdsSize]; - for (var i = 0; i < activeDbIdsSize; i++) - { - dbId = activeDbIdsSnapshot[i]; - var db = databasesMapSnapshot[dbId]; - tasks[i] = db.AppendOnlyFile.CommitAsync(token: token).AsTask(); - } - - await Task.WhenAll(tasks).ContinueWith(_ => databasesLock.ReadUnlock(), token); + await databaseManager.CommitToAofAsync(dbId, token); } internal void Start() @@ -643,8 +498,8 @@ internal void Start() if (serverOptions.AofSizeLimit.Length > 0) { - var AofSizeLimitBytes = 1L << serverOptions.AofSizeLimitSizeBits(); - Task.Run(async () => await AutoCheckpointBasedOnAofSizeLimit(AofSizeLimitBytes, ctsCommit.Token, logger)); + var aofSizeLimitBytes = 1L << serverOptions.AofSizeLimitSizeBits(); + Task.Run(async () => await AutoCheckpointBasedOnAofSizeLimit(aofSizeLimitBytes, ctsCommit.Token, logger)); } if (serverOptions.CommitFrequencyMs > 0 && appendOnlyFile != null) @@ -667,22 +522,7 @@ internal void Start() Task.Run(() => IndexAutoGrowTask(ctsCommit.Token)); } - objectStoreSizeTracker?.Start(ctsCommit.Token); - - var activeDbIdsSize = activeDbIdsLength; - - if (allowMultiDb && activeDbIdsSize > 1) - { - var databasesMapSnapshot = databases.Map; - var activeDbIdsSnapshot = activeDbIds; - - for (var i = 1; i < activeDbIdsSize; i++) - { - var dbId = activeDbIdsSnapshot[i]; - var db = databasesMapSnapshot[dbId]; - db.ObjectStoreSizeTracker?.Start(ctsCommit.Token); - } - } + databaseManager.StartObjectSizeTrackers(ctsCommit.Token); } /// Grows indexes of both main store and object store if current size is too small. @@ -695,71 +535,11 @@ private async void IndexAutoGrowTask(CancellationToken token) while (!allIndexesMaxedOut) { - allIndexesMaxedOut = true; - if (token.IsCancellationRequested) break; await Task.Delay(TimeSpan.FromSeconds(serverOptions.IndexResizeFrequencySecs), token); - var lockAcquired = TryGetDatabasesReadLockAsync(token).Result; - if (!lockAcquired) return; - - try - { - var activeDbIdsSize = activeDbIdsLength; - var activeDbIdsSnapshot = activeDbIds; - - var databasesMapSnapshot = databases.Map; - - for (var i = 0; i < activeDbIdsSize; i++) - { - var dbId = activeDbIdsSnapshot[i]; - var db = databasesMapSnapshot[dbId]; - var dbUpdated = false; - - if (!db.MainStoreIndexMaxedOut) - { - var dbMainStore = databasesMapSnapshot[dbId].MainStore; - if (GrowIndexIfNeeded(StoreType.Main, - serverOptions.AdjustedIndexMaxCacheLines, dbMainStore.OverflowBucketAllocations, - () => dbMainStore.IndexSize, () => dbMainStore.GrowIndex())) - { - db.MainStoreIndexMaxedOut = true; - dbUpdated = true; - } - else - { - allIndexesMaxedOut = false; - } - } - - if (!db.ObjectStoreIndexMaxedOut) - { - var dbObjectStore = databasesMapSnapshot[dbId].ObjectStore; - if (GrowIndexIfNeeded(StoreType.Object, - serverOptions.AdjustedObjectStoreIndexMaxCacheLines, - dbObjectStore.OverflowBucketAllocations, - () => dbObjectStore.IndexSize, () => dbObjectStore.GrowIndex())) - { - db.ObjectStoreIndexMaxedOut = true; - dbUpdated = true; - } - else - { - allIndexesMaxedOut = false; - } - } - - if (dbUpdated) - { - databasesMapSnapshot[dbId] = db; - } - } - } - finally - { - databasesLock.ReadUnlock(); - } + allIndexesMaxedOut = databaseManager.GrowIndexesIfNeeded(token); } } catch (Exception ex) @@ -768,34 +548,6 @@ private async void IndexAutoGrowTask(CancellationToken token) } } - /// - /// Grows index if current size is smaller than max size. - /// Decision is based on whether overflow bucket allocation is more than a threshold which indicates a contention - /// in the index leading many allocations to the same bucket. - /// - /// - /// - /// - /// - /// - /// True if index has reached its max size - private bool GrowIndexIfNeeded(StoreType storeType, long indexMaxSize, long overflowCount, Func indexSizeRetriever, Action growAction) - { - logger?.LogDebug($"{nameof(IndexAutoGrowTask)}[{{storeType}}]: checking index size {{indexSizeRetriever}} against max {{indexMaxSize}} with overflow {{overflowCount}}", storeType, indexSizeRetriever(), indexMaxSize, overflowCount); - - if (indexSizeRetriever() < indexMaxSize && - overflowCount > (indexSizeRetriever() * serverOptions.IndexResizeThreshold / 100)) - { - logger?.LogInformation($"{nameof(IndexAutoGrowTask)}[{{storeType}}]: overflowCount {{overflowCount}} ratio more than threshold {{indexResizeThreshold}}%. Doubling index size...", storeType, overflowCount, serverOptions.IndexResizeThreshold); - growAction(); - } - - if (indexSizeRetriever() < indexMaxSize) return false; - - logger?.LogDebug($"{nameof(IndexAutoGrowTask)}[{{storeType}}]: index size {{indexSizeRetriever}} reached index max size {{indexMaxSize}}", storeType, indexSizeRetriever(), indexMaxSize); - return true; - } - /// /// Dispose /// From 987bdb97227a3c67e9e28c9e36af8956e2a7eb6a Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 27 Feb 2025 17:15:48 -0800 Subject: [PATCH 58/82] wip --- libs/resources/RespCommandsDocs.json | 19 +++++ libs/resources/RespCommandsInfo.json | 7 ++ libs/server/Databases/DatabaseManagerBase.cs | 2 + libs/server/Databases/MultiDatabaseManager.cs | 72 ++++++++++++------- .../server/Databases/SingleDatabaseManager.cs | 8 ++- libs/server/Resp/AdminCommands.cs | 4 +- libs/server/Resp/RespServerSession.cs | 2 +- libs/server/StoreWrapper.cs | 24 ++++++- test/Garnet.test/MultiDatabaseTests.cs | 5 +- 9 files changed, 106 insertions(+), 37 deletions(-) diff --git a/libs/resources/RespCommandsDocs.json b/libs/resources/RespCommandsDocs.json index 4f482776fe..1f4bf50a0d 100644 --- a/libs/resources/RespCommandsDocs.json +++ b/libs/resources/RespCommandsDocs.json @@ -6890,6 +6890,25 @@ } ] }, + { + "Command": "SWAPDB", + "Name": "SWAPDB", + "Summary": "Swaps two Memurai databases", + "Group": "Server", + "Complexity": "O(N) where N is the count of clients watching or blocking on keys from both databases.", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "INDEX1", + "Type": "Integer" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "INDEX2", + "Type": "Integer" + } + ] + }, { "Command": "TIME", "Name": "TIME", diff --git a/libs/resources/RespCommandsInfo.json b/libs/resources/RespCommandsInfo.json index ba56069d90..3166e142be 100644 --- a/libs/resources/RespCommandsInfo.json +++ b/libs/resources/RespCommandsInfo.json @@ -4591,6 +4591,13 @@ } ] }, + { + "Command": "SWAPDB", + "Name": "SWAPDB", + "Arity": 3, + "Flags": "Fast, Write", + "AclCategories": "Dangerous, Fast, KeySpace, Write" + }, { "Command": "TIME", "Name": "TIME", diff --git a/libs/server/Databases/DatabaseManagerBase.cs b/libs/server/Databases/DatabaseManagerBase.cs index e362f8889e..6783497865 100644 --- a/libs/server/Databases/DatabaseManagerBase.cs +++ b/libs/server/Databases/DatabaseManagerBase.cs @@ -156,6 +156,8 @@ protected DatabaseManagerBase(StoreWrapper.DatabaseCreatorDelegate createDatabas this.LoggerFactory = storeWrapper.loggerFactory; } + protected abstract ref GarnetDatabase GetDatabaseByRef(int dbId = 0); + protected void RecoverDatabaseCheckpoint(ref GarnetDatabase db, out long storeVersion, out long objectStoreVersion) { storeVersion = db.MainStore.Recover(); diff --git a/libs/server/Databases/MultiDatabaseManager.cs b/libs/server/Databases/MultiDatabaseManager.cs index 3690fb7075..cebd36c03b 100644 --- a/libs/server/Databases/MultiDatabaseManager.cs +++ b/libs/server/Databases/MultiDatabaseManager.cs @@ -55,8 +55,8 @@ internal class MultiDatabaseManager : DatabaseManagerBase readonly string checkpointDirBaseName; - public MultiDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabaseDelegate, - StoreWrapper storeWrapper, bool createDefaultDatabase = true) : base(createsDatabaseDelegate, storeWrapper) + public MultiDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createDatabaseDelegate, + StoreWrapper storeWrapper, bool createDefaultDatabase = true) : base(createDatabaseDelegate, storeWrapper) { maxDatabases = storeWrapper.serverOptions.MaxDatabases; Logger = storeWrapper.loggerFactory?.CreateLogger(nameof(MultiDatabaseManager)); @@ -67,7 +67,7 @@ public MultiDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabase // Create default database of index 0 (unless specified otherwise) if (createDefaultDatabase) { - var db = CreateDatabaseDelegate(0, out var storeCheckpointDir, out var aofDir); + var db = createDatabaseDelegate(0, out var storeCheckpointDir, out var aofDir); var checkpointDirInfo = new DirectoryInfo(storeCheckpointDir); checkpointDirBaseName = checkpointDirInfo.Name; @@ -525,6 +525,15 @@ public override bool TrySwapDatabases(int dbId1, int dbId2) /// public override IDatabaseManager Clone(bool enableAof) => new MultiDatabaseManager(this, enableAof); + protected override ref GarnetDatabase GetDatabaseByRef(int dbId = 0) + { + var databasesMapSize = databases.ActualSize; + var databasesMapSnapshot = databases.Map; + Debug.Assert(dbId < databasesMapSize && !databasesMapSnapshot[dbId].IsDefault()); + + return ref databasesMapSnapshot[dbId]; + } + public override FunctionsState CreateFunctionsState(int dbId = 0) { if (!TryGetOrAddDatabase(dbId, out var db)) @@ -558,15 +567,16 @@ public override bool TryPauseCheckpoints(int dbId) /// public override async Task TryPauseCheckpointsContinuousAsync(int dbId, CancellationToken token = default) { - if (!TryGetOrAddDatabase(dbId, out var db)) - throw new GarnetException($"Database with ID {dbId} was not found."); + var databasesMapSize = databases.ActualSize; + var databasesMapSnapshot = databases.Map; + Debug.Assert(dbId < databasesMapSize && !databasesMapSnapshot[dbId].IsDefault()); - var checkpointsPaused = TryPauseCheckpoints(ref db); + var checkpointsPaused = TryPauseCheckpoints(ref databasesMapSnapshot[dbId]); while (!checkpointsPaused && !token.IsCancellationRequested && !Disposed) { await Task.Yield(); - checkpointsPaused = TryPauseCheckpoints(ref db); + checkpointsPaused = TryPauseCheckpoints(ref databasesMapSnapshot[dbId]); } return checkpointsPaused; @@ -575,10 +585,11 @@ public override async Task TryPauseCheckpointsContinuousAsync(int dbId, Ca /// public override void ResumeCheckpoints(int dbId) { - if (!TryGetOrAddDatabase(dbId, out var db)) - throw new GarnetException($"Database with ID {dbId} was not found."); + var databasesMapSize = databases.ActualSize; + var databasesMapSnapshot = databases.Map; + Debug.Assert(dbId < databasesMapSize && !databasesMapSnapshot[dbId].IsDefault()); - ResumeCheckpoints(ref db); + ResumeCheckpoints(ref databasesMapSnapshot[dbId]); } /// @@ -814,35 +825,42 @@ private async Task TakeDatabasesCheckpointAsync(StoreType storeType, int dbIdsCo for (var i = 0; i < checkpointTasks.Length; i++) checkpointTasks[i] = Task.CompletedTask; - var databaseMapSnapshot = databases.Map; + var lockAcquired = await TryGetDatabasesReadLockAsync(token); + if (!lockAcquired) return; - var currIdx = 0; - while (currIdx < dbIdsCount) + try { - var dbId = dbIdsToCheckpoint[currIdx]; + var databaseMapSnapshot = databases.Map; - // Prevent parallel checkpoint - if (!await TryPauseCheckpointsContinuousAsync(dbId, token)) - continue; + var currIdx = 0; + while (currIdx < dbIdsCount) + { + var dbId = dbIdsToCheckpoint[currIdx]; - checkpointTasks[currIdx] = TakeCheckpointAsync(databaseMapSnapshot[dbId], storeType, logger: logger, token: token).ContinueWith( - _ => - { - TryUpdateLastSaveTimeAsync(dbId, token).GetAwaiter().GetResult(); - ResumeCheckpoints(dbId); - }, TaskContinuationOptions.ExecuteSynchronously); + // Prevent parallel checkpoint + if (!await TryPauseCheckpointsContinuousAsync(dbId, token)) + continue; - currIdx++; - } + checkpointTasks[currIdx] = TakeCheckpointAsync(databaseMapSnapshot[dbId], storeType, logger: logger, token: token).ContinueWith( + _ => + { + TryUpdateLastSaveTimeAsync(dbId, token).GetAwaiter().GetResult(); + ResumeCheckpoints(dbId); + }, TaskContinuationOptions.ExecuteSynchronously); + + currIdx++; + } - try - { await Task.WhenAll(checkpointTasks); } catch (Exception ex) { logger?.LogError(ex, "Checkpointing threw exception"); } + finally + { + databasesLock.ReadUnlock(); + } } private async Task TryUpdateLastSaveTimeAsync(int dbId, CancellationToken token = default) diff --git a/libs/server/Databases/SingleDatabaseManager.cs b/libs/server/Databases/SingleDatabaseManager.cs index 64ef1e3743..93d8031644 100644 --- a/libs/server/Databases/SingleDatabaseManager.cs +++ b/libs/server/Databases/SingleDatabaseManager.cs @@ -19,15 +19,15 @@ internal class SingleDatabaseManager : DatabaseManagerBase GarnetDatabase defaultDatabase; - public SingleDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createsDatabaseDelegate, StoreWrapper storeWrapper, bool createDefaultDatabase = true) : - base(createsDatabaseDelegate, storeWrapper) + public SingleDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createDatabaseDelegate, StoreWrapper storeWrapper, bool createDefaultDatabase = true) : + base(createDatabaseDelegate, storeWrapper) { Logger = storeWrapper.loggerFactory?.CreateLogger(nameof(SingleDatabaseManager)); // Create default database of index 0 (unless specified otherwise) if (createDefaultDatabase) { - defaultDatabase = createsDatabaseDelegate(0, out _, out _); + defaultDatabase = createDatabaseDelegate(0, out _, out _); } } @@ -301,6 +301,8 @@ public override void FlushAllDatabases(bool unsafeTruncateLog) => /// public override IDatabaseManager Clone(bool enableAof) => new SingleDatabaseManager(this, enableAof); + protected override ref GarnetDatabase GetDatabaseByRef(int dbId = 0) => ref DefaultDatabase; + public override FunctionsState CreateFunctionsState(int dbId = 0) { ArgumentOutOfRangeException.ThrowIfNotEqual(dbId, 0); diff --git a/libs/server/Resp/AdminCommands.cs b/libs/server/Resp/AdminCommands.cs index ec9c4773fb..aea8de2c90 100644 --- a/libs/server/Resp/AdminCommands.cs +++ b/libs/server/Resp/AdminCommands.cs @@ -878,7 +878,7 @@ private bool NetworkSAVE() } } - if (!storeWrapper.databaseManager.TakeCheckpoint(false, dbId: dbId, logger: logger)) + if (!storeWrapper.TakeCheckpoint(false, dbId: dbId, logger: logger)) { while (!RespWriteUtils.TryWriteError("ERR checkpoint already in progress"u8, ref dcurr, dend)) SendAndReset(); @@ -989,7 +989,7 @@ private bool NetworkBGSAVE() } } - var success = storeWrapper.databaseManager.TakeCheckpoint(true, dbId: dbId, logger: logger); + var success = storeWrapper.TakeCheckpoint(true, dbId: dbId, logger: logger); if (success) { while (!RespWriteUtils.TryWriteSimpleString("Background saving started"u8, ref dcurr, dend)) diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index 9da87a2e27..af16edfbc5 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -235,7 +235,7 @@ public RespServerSession( { databaseSessions = new ExpandableMap(1, 0, maxDbs - 1); if (!databaseSessions.TrySetValue(0, ref dbSession)) - throw new GarnetException("Failed to set initial database in databases map"); + throw new GarnetException("Failed to set initial database session in database sessions map"); } SwitchActiveDatabaseSession(0, ref dbSession); diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 2fe8df3dce..1dbffa320a 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -126,6 +126,8 @@ public sealed class StoreWrapper // Standalone instance node_id internal readonly string runId; + private readonly DatabaseCreatorDelegate createDatabaseDelegate; + readonly CancellationTokenSource ctsCommit; // True if StoreWrapper instance is disposed @@ -153,6 +155,7 @@ public StoreWrapper( this.serverOptions = serverOptions; this.subscribeBroker = subscribeBroker; this.customCommandManager = customCommandManager; + this.createDatabaseDelegate = createDatabaseDelegate; this.databaseManager = serverOptions.EnableCluster || serverOptions.MaxDatabases == 1 ? new SingleDatabaseManager(createDatabaseDelegate, this) : new MultiDatabaseManager(createDatabaseDelegate, this); @@ -225,7 +228,7 @@ public StoreWrapper(StoreWrapper storeWrapper, bool recordToAof) : this( storeWrapper.version, storeWrapper.redisProtocolVersion, storeWrapper.server, - null, + storeWrapper.createDatabaseDelegate, storeWrapper.customCommandManager, storeWrapper.serverOptions, storeWrapper.subscribeBroker, @@ -491,6 +494,25 @@ internal async ValueTask CommitAOFAsync(int dbId = -1, CancellationToken token = await databaseManager.CommitToAofAsync(dbId, token); } + /// + /// Take checkpoint of all active database IDs or a specified database ID + /// + /// True if method can return before checkpoint is taken + /// ID of database to checkpoint (default: -1 - checkpoint all active databases) + /// Store type to checkpoint + /// Logger + /// Cancellation token + /// False if another checkpointing process is already in progress + public bool TakeCheckpoint(bool background, int dbId = -1, StoreType storeType = StoreType.All, ILogger logger = null, CancellationToken token = default) + { + if (dbId == -1) + { + return databaseManager.TakeCheckpoint(background, storeType, logger, token); + } + + return databaseManager.TakeCheckpoint(background, dbId, storeType, logger, token); + } + internal void Start() { monitor?.Start(); diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index 11df0b083e..5024f4946c 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -18,7 +18,7 @@ public class MultiDatabaseTests [SetUp] public void Setup() { - TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); + TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: false); server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, enableAOF: true, lowMemory: true, commitFrequencyMs: 1000); server.Start(); } @@ -899,7 +899,6 @@ public void MultiDatabaseSaveRecoverByDbIdTest(bool backgroundSave) Task.Delay(TimeSpan.FromMilliseconds(100), cts.Token); } - var prevLastSave = expectedLastSave; expectedLastSave = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); Assert.That(lastSave, Is.InRange(expectedLastSave - 2, expectedLastSave + 2)); @@ -908,7 +907,7 @@ public void MultiDatabaseSaveRecoverByDbIdTest(bool backgroundSave) lastSaveStr = db1.Execute("LASTSAVE", "1").ToString(); parsed = long.TryParse(lastSaveStr, out lastSave); ClassicAssert.IsTrue(parsed); - ClassicAssert.AreEqual(prevLastSave, lastSave); + ClassicAssert.AreEqual(0, lastSave); } // Restart server From 2acfcc4c0fdb77c64bdea3d70881483e4fde2c46 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 27 Feb 2025 22:07:11 -0800 Subject: [PATCH 59/82] wip --- libs/server/Databases/MultiDatabaseManager.cs | 3 +-- libs/server/GarnetDatabase.cs | 6 +++--- libs/server/StoreWrapper.cs | 1 + test/Garnet.test/MultiDatabaseTests.cs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/server/Databases/MultiDatabaseManager.cs b/libs/server/Databases/MultiDatabaseManager.cs index cebd36c03b..c97bb83665 100644 --- a/libs/server/Databases/MultiDatabaseManager.cs +++ b/libs/server/Databases/MultiDatabaseManager.cs @@ -886,6 +886,7 @@ public override void Dispose() if (Disposed) return; cts.Cancel(); + Disposed = true; // Disable changes to databases map and dispose all databases databases.mapLock.WriteLock(); @@ -893,8 +894,6 @@ public override void Dispose() db.Dispose(); cts.Dispose(); - - Disposed = true; } } } diff --git a/libs/server/GarnetDatabase.cs b/libs/server/GarnetDatabase.cs index 5232f0a4e0..77f31328fd 100644 --- a/libs/server/GarnetDatabase.cs +++ b/libs/server/GarnetDatabase.cs @@ -130,6 +130,9 @@ public void Dispose() { if (disposed) return; + // Wait for checkpoints to complete and disable checkpointing + CheckpointingLock.CloseLock(); + MainStore?.Dispose(); ObjectStore?.Dispose(); AofDevice?.Dispose(); @@ -141,9 +144,6 @@ public void Dispose() Thread.Yield(); } - // Wait for checkpoints to complete and disable checkpointing - CheckpointingLock.CloseLock(); - disposed = true; } } diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 1dbffa320a..bda4116ac0 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -581,6 +581,7 @@ public void Dispose() itemBroker?.Dispose(); monitor?.Dispose(); ctsCommit?.Cancel(); + databaseManager.Dispose(); ctsCommit?.Dispose(); clusterProvider?.Dispose(); diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index 5024f4946c..77c117ff94 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -18,7 +18,7 @@ public class MultiDatabaseTests [SetUp] public void Setup() { - TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: false); + TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, enableAOF: true, lowMemory: true, commitFrequencyMs: 1000); server.Start(); } From 32565ce7c0d327ba61d969cf38acadb242773b3f Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Mon, 3 Mar 2025 14:43:22 -0800 Subject: [PATCH 60/82] Fixing SWAPDB --- libs/server/Databases/MultiDatabaseManager.cs | 10 ++++++++++ libs/server/Resp/ArrayCommands.cs | 2 +- libs/server/Resp/RespServerSession.cs | 7 +++---- test/Garnet.test/MultiDatabaseTests.cs | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/libs/server/Databases/MultiDatabaseManager.cs b/libs/server/Databases/MultiDatabaseManager.cs index c97bb83665..4bf4ba0855 100644 --- a/libs/server/Databases/MultiDatabaseManager.cs +++ b/libs/server/Databases/MultiDatabaseManager.cs @@ -503,6 +503,8 @@ public override void EnqueueCommit(bool isMainStore, long version, int dbId = 0) /// public override bool TrySwapDatabases(int dbId1, int dbId2) { + if (dbId1 == dbId2) return true; + if (!TryGetOrAddDatabase(dbId1, out var db1) || !TryGetOrAddDatabase(dbId2, out var db2)) return false; @@ -513,6 +515,14 @@ public override bool TrySwapDatabases(int dbId1, int dbId2) var databaseMapSnapshot = databases.Map; databaseMapSnapshot[dbId2] = db1; databaseMapSnapshot[dbId1] = db2; + + var sessions = StoreWrapper.TcpServer.ActiveConsumers(); + foreach (var session in sessions) + { + if (session is not RespServerSession respServerSession) continue; + + respServerSession.TrySwapDatabaseSessions(dbId1, dbId2); + } } finally { diff --git a/libs/server/Resp/ArrayCommands.cs b/libs/server/Resp/ArrayCommands.cs index 47c4ff6187..3f839d05ec 100644 --- a/libs/server/Resp/ArrayCommands.cs +++ b/libs/server/Resp/ArrayCommands.cs @@ -283,7 +283,7 @@ private bool NetworkSWAPDB() if (index1 < storeWrapper.serverOptions.MaxDatabases && index2 < storeWrapper.serverOptions.MaxDatabases) { - if (this.TrySwapDatabases(index1, index2)) + if (storeWrapper.TrySwapDatabases(index1, index2)) { while (!RespWriteUtils.TryWriteDirect(CmdStrings.RESP_OK, ref dcurr, dend)) SendAndReset(); diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index af16edfbc5..a3eb8413c6 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -1276,14 +1276,13 @@ internal bool TrySwitchActiveDatabaseSession(int dbId) return true; } - internal bool TrySwapDatabases(int dbId1, int dbId2) + internal bool TrySwapDatabaseSessions(int dbId1, int dbId2) { if (!allowMultiDb) return false; if (dbId1 == dbId2) return true; - if (!storeWrapper.TrySwapDatabases(dbId1, dbId2)) return false; - if (!databaseSessions.TryGetOrSet(dbId1, () => CreateDatabaseSession(dbId1), out var dbSession1, out _) || - !databaseSessions.TryGetOrSet(dbId2, () => CreateDatabaseSession(dbId2), out var dbSession2, out _)) + if (!databaseSessions.TryGetOrSet(dbId1, () => CreateDatabaseSession(dbId2), out var dbSession1, out _) || + !databaseSessions.TryGetOrSet(dbId2, () => CreateDatabaseSession(dbId1), out var dbSession2, out _)) return false; databaseSessions.Map[dbId1] = dbSession2; diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index 77c117ff94..5a93c188b6 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -370,7 +370,7 @@ public void MultiDatabaseSwapDatabasesTestLC() } [Test] - [Ignore("")] + //[Ignore("")] public void MultiDatabaseMultiSessionSwapDatabasesTestLC() { var db1Key1 = "db1:key1"; From 1ab57d4df3cd6307ca6baa83d38dc8811ef295f6 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Mon, 3 Mar 2025 15:26:56 -0800 Subject: [PATCH 61/82] wip --- libs/server/Databases/MultiDatabaseManager.cs | 23 ++++++++++++++++--- .../server/Databases/SingleDatabaseManager.cs | 12 +++++++++- libs/server/Providers/GarnetProvider.cs | 2 +- .../Providers/TsavoriteKVProviderBase.cs | 16 ++----------- libs/server/Resp/Parser/RespCommand.cs | 2 +- libs/server/Resp/RespServerSession.cs | 2 +- test/Garnet.test/MultiDatabaseTests.cs | 1 - 7 files changed, 36 insertions(+), 22 deletions(-) diff --git a/libs/server/Databases/MultiDatabaseManager.cs b/libs/server/Databases/MultiDatabaseManager.cs index 4bf4ba0855..9864143161 100644 --- a/libs/server/Databases/MultiDatabaseManager.cs +++ b/libs/server/Databases/MultiDatabaseManager.cs @@ -271,7 +271,7 @@ public override async Task CommitToAofAsync(CancellationToken token = default, I var lockAcquired = TryGetDatabasesReadLockAsync(token).Result; if (!lockAcquired) return; - var aofTasks = new Task[activeDbIdsSize]; + var aofTasks = new Task<(long, long)>[activeDbIdsSize]; try { @@ -281,10 +281,27 @@ public override async Task CommitToAofAsync(CancellationToken token = default, I var db = databasesMapSnapshot[dbId]; Debug.Assert(!db.IsDefault()); - aofTasks[i] = db.AppendOnlyFile.CommitAsync(token: token).AsTask(); + aofTasks[i] = db.AppendOnlyFile.CommitAsync(token: token).AsTask().ContinueWith(_ => (db.AppendOnlyFile.TailAddress, db.AppendOnlyFile.CommittedUntilAddress), token); } - await Task.WhenAll(aofTasks); + try + { + await Task.WhenAll(aofTasks); + } + catch (Exception) + { + // Only first exception is caught here, if any. + // Proper handling of this and consequent exceptions in the next loop. + } + + foreach (var t in aofTasks) + { + if (!t.IsFaulted || t.Exception == null) continue; + + logger?.LogError(t.Exception, + "Exception raised while committing to AOF. AOF tail address = {tailAddress}; AOF committed until address = {commitAddress}; ", + t.Result.Item1, t.Result.Item2); + } } finally { diff --git a/libs/server/Databases/SingleDatabaseManager.cs b/libs/server/Databases/SingleDatabaseManager.cs index 93d8031644..1cf73646ba 100644 --- a/libs/server/Databases/SingleDatabaseManager.cs +++ b/libs/server/Databases/SingleDatabaseManager.cs @@ -209,7 +209,17 @@ public override async Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLi /// public override async Task CommitToAofAsync(CancellationToken token = default, ILogger logger = null) { - await AppendOnlyFile.CommitAsync(token: token); + try + { + await AppendOnlyFile.CommitAsync(token: token); + } + catch (Exception ex) + { + logger?.LogError(ex, + "Exception raised while committing to AOF. AOF tail address = {tailAddress}; AOF committed until address = {commitAddress}; ", + AppendOnlyFile.TailAddress, AppendOnlyFile.CommittedUntilAddress); + throw; + } } /// diff --git a/libs/server/Providers/GarnetProvider.cs b/libs/server/Providers/GarnetProvider.cs index d9c28b7c97..1ccbc6ac24 100644 --- a/libs/server/Providers/GarnetProvider.cs +++ b/libs/server/Providers/GarnetProvider.cs @@ -35,7 +35,7 @@ public sealed class GarnetProvider : TsavoriteKVProviderBase /// Create TsavoriteKV backend /// - /// /// /// - /// /// - public TsavoriteKVProviderBase(TsavoriteKV store, TParameterSerializer serializer, - SubscribeBroker broker = null, bool recoverStore = false, MaxSizeSettings maxSizeSettings = default) + public TsavoriteKVProviderBase(TParameterSerializer serializer, + SubscribeBroker broker = null, MaxSizeSettings maxSizeSettings = default) { - this.store = store; - if (recoverStore) - { - try - { - store.Recover(); - } - catch - { } - } this.broker = broker; this.serializer = serializer; this.maxSizeSettings = maxSizeSettings ?? new MaxSizeSettings(); diff --git a/libs/server/Resp/Parser/RespCommand.cs b/libs/server/Resp/Parser/RespCommand.cs index bb96e87c63..61aa0134b0 100644 --- a/libs/server/Resp/Parser/RespCommand.cs +++ b/libs/server/Resp/Parser/RespCommand.cs @@ -2529,7 +2529,7 @@ private RespCommand ParseCommand(out bool success) } endReadHead = (int)(ptr - recvBufferPtr); - if (storeWrapper.appendOnlyFile != null && storeWrapper.serverOptions.WaitForCommit) + if (storeWrapper.serverOptions.EnableAOF && storeWrapper.serverOptions.WaitForCommit) HandleAofCommitMode(cmd); return cmd; diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index a3eb8413c6..74915da7f5 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -1211,7 +1211,7 @@ private void DebugSend(byte* d) if ((int)(dcurr - d) > 0) { - if (storeWrapper.appendOnlyFile != null && storeWrapper.serverOptions.WaitForCommit) + if (storeWrapper.serverOptions.EnableAOF && storeWrapper.serverOptions.WaitForCommit) { var task = storeWrapper.WaitForCommitAsync(); if (!task.IsCompleted) task.AsTask().GetAwaiter().GetResult(); diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index 5a93c188b6..426c0e4015 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -370,7 +370,6 @@ public void MultiDatabaseSwapDatabasesTestLC() } [Test] - //[Ignore("")] public void MultiDatabaseMultiSessionSwapDatabasesTestLC() { var db1Key1 = "db1:key1"; From 102d65a0c8a68551e982088be26d0e9f538f174e Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Mon, 3 Mar 2025 15:43:49 -0800 Subject: [PATCH 62/82] wip --- libs/server/Databases/MultiDatabaseManager.cs | 5 +++++ libs/server/StoreWrapper.cs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/libs/server/Databases/MultiDatabaseManager.cs b/libs/server/Databases/MultiDatabaseManager.cs index 9864143161..28428d46e4 100644 --- a/libs/server/Databases/MultiDatabaseManager.cs +++ b/libs/server/Databases/MultiDatabaseManager.cs @@ -284,6 +284,7 @@ public override async Task CommitToAofAsync(CancellationToken token = default, I aofTasks[i] = db.AppendOnlyFile.CommitAsync(token: token).AsTask().ContinueWith(_ => (db.AppendOnlyFile.TailAddress, db.AppendOnlyFile.CommittedUntilAddress), token); } + var exThrown = false; try { await Task.WhenAll(aofTasks); @@ -292,6 +293,7 @@ public override async Task CommitToAofAsync(CancellationToken token = default, I { // Only first exception is caught here, if any. // Proper handling of this and consequent exceptions in the next loop. + exThrown = true; } foreach (var t in aofTasks) @@ -302,6 +304,9 @@ public override async Task CommitToAofAsync(CancellationToken token = default, I "Exception raised while committing to AOF. AOF tail address = {tailAddress}; AOF committed until address = {commitAddress}; ", t.Result.Item1, t.Result.Item2); } + + if (exThrown) + throw new GarnetException($"Error occurred while committing to AOF in {nameof(MultiDatabaseManager)}. Refer to previous log messages for more details."); } finally { diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index bda4116ac0..f9fb141909 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -370,7 +370,7 @@ async Task CommitTask(int commitFrequencyMs, ILogger logger = null, Cancellation } catch (Exception ex) { - logger?.LogError(ex, "CommitTask exception received, AOF tail address = {tailAddress}; AOF committed until address = {commitAddress}; ", appendOnlyFile.TailAddress, appendOnlyFile.CommittedUntilAddress); + logger?.LogError(ex, "CommitTask exception received."); } } From c27ab010110c6790693e7825141996e9afef8834 Mon Sep 17 00:00:00 2001 From: Vasileios Zois Date: Tue, 4 Mar 2025 13:42:05 -0800 Subject: [PATCH 63/82] add explicit fail on fail to dispose --- test/Garnet.test.cluster/ClusterTestContext.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/Garnet.test.cluster/ClusterTestContext.cs b/test/Garnet.test.cluster/ClusterTestContext.cs index 011b0a25ed..f8d2cfee94 100644 --- a/test/Garnet.test.cluster/ClusterTestContext.cs +++ b/test/Garnet.test.cluster/ClusterTestContext.cs @@ -66,9 +66,15 @@ public void TearDown() clusterTestUtils?.Dispose(); loggerFactory?.Dispose(); if (!Task.Run(() => DisposeCluster()).Wait(TimeSpan.FromSeconds(15))) + { logger?.LogError("Timed out waiting for DisposeCluster"); + Assert.Fail("Timed out waiting for DisposeCluster"); + } if (!Task.Run(() => TestUtils.DeleteDirectory(TestFolder, true)).Wait(TimeSpan.FromSeconds(15))) - logger?.LogError("Timed out waiting for DisposeCluster"); + { + logger?.LogError("Timed out DeleteDirectory"); + Assert.Fail("Timed out DeleteDirectory"); + } } public void RegisterCustomTxn(string name, Func proc, RespCommandsInfo commandInfo = null, RespCommandDocs commandDocs = null) From 56a8625f60dedf455998498d62d4eefcf7dc4603 Mon Sep 17 00:00:00 2001 From: Vasileios Zois Date: Tue, 4 Mar 2025 14:50:39 -0800 Subject: [PATCH 64/82] replayStoreWrapper should not create database --- libs/server/StoreWrapper.cs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index f9fb141909..8754848541 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -224,19 +224,18 @@ public StoreWrapper( /// /// Source instance /// Enable AOF in StoreWrapper copy - public StoreWrapper(StoreWrapper storeWrapper, bool recordToAof) : this( - storeWrapper.version, - storeWrapper.redisProtocolVersion, - storeWrapper.server, - storeWrapper.createDatabaseDelegate, - storeWrapper.customCommandManager, - storeWrapper.serverOptions, - storeWrapper.subscribeBroker, - storeWrapper.accessControlList, - null, - storeWrapper.loggerFactory) + public StoreWrapper(StoreWrapper storeWrapper, bool recordToAof) { + this.version = storeWrapper.version; + this.redisProtocolVersion = storeWrapper.version; + this.server = storeWrapper.server; + this.startupTime = DateTimeOffset.UtcNow.Ticks; + this.serverOptions = storeWrapper.serverOptions; + this.subscribeBroker = storeWrapper.subscribeBroker; + this.customCommandManager = storeWrapper.customCommandManager; + this.loggerFactory = storeWrapper.loggerFactory; this.databaseManager = storeWrapper.databaseManager.Clone(recordToAof); + this.accessControlList = storeWrapper.accessControlList; } /// From 9f5ff7b40f9859cecf38c73d29e794c19a949dd7 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 4 Mar 2025 15:05:55 -0800 Subject: [PATCH 65/82] wip --- libs/server/AOF/AofProcessor.cs | 30 ++-- libs/server/Databases/DatabaseManagerBase.cs | 33 +++-- libs/server/Databases/IDatabaseManager.cs | 13 +- libs/server/Databases/MultiDatabaseManager.cs | 47 +++++- .../server/Databases/SingleDatabaseManager.cs | 11 +- libs/server/Metrics/GarnetServerMonitor.cs | 3 +- libs/server/Metrics/Info/GarnetInfoMetrics.cs | 137 +++++++++++------- libs/server/Resp/RespServerSession.cs | 7 +- libs/server/StoreWrapper.cs | 21 ++- 9 files changed, 205 insertions(+), 97 deletions(-) diff --git a/libs/server/AOF/AofProcessor.cs b/libs/server/AOF/AofProcessor.cs index 09c25bbb96..72f8aae373 100644 --- a/libs/server/AOF/AofProcessor.cs +++ b/libs/server/AOF/AofProcessor.cs @@ -35,11 +35,6 @@ public sealed unsafe partial class AofProcessor int activeDbId; - /// - /// Replication offset - /// - internal long ReplicationOffset { get; private set; } - /// /// Session for main store /// @@ -66,7 +61,6 @@ public AofProcessor( ILogger logger = null) { this.storeWrapper = storeWrapper; - ReplicationOffset = 0; var replayAofStoreWrapper = new StoreWrapper(storeWrapper, recordToAof); @@ -91,22 +85,30 @@ public AofProcessor( /// public void Dispose() { - basicContext.Session?.Dispose(); - objectStoreBasicContext.Session?.Dispose(); + var databaseSessionsSnapshot = respServerSession.databaseSessions.Map; + foreach (var dbSession in databaseSessionsSnapshot) + { + dbSession.StorageSession.basicContext.Session?.Dispose(); + dbSession.StorageSession.objectStoreBasicContext.Session?.Dispose(); + } + handle.Free(); } /// /// Recover store using AOF /// - public unsafe void Recover(ref GarnetDatabase db, long untilAddress = -1) + /// Database to recover + /// + /// Replication offset + public unsafe long Recover(ref GarnetDatabase db, long untilAddress = -1) { logger?.LogInformation("Begin AOF recovery"); - RecoverReplay(ref db, untilAddress); + return RecoverReplay(ref db, untilAddress); } MemoryResult output = default; - private unsafe void RecoverReplay(ref GarnetDatabase db, long untilAddress) + private unsafe long RecoverReplay(ref GarnetDatabase db, long untilAddress) { logger?.LogInformation("Begin AOF replay"); try @@ -127,10 +129,8 @@ private unsafe void RecoverReplay(ref GarnetDatabase db, long untilAddress) logger?.LogInformation("Completed AOF replay of {count} records, until AOF address {nextAofAddress}", count, nextAofAddress); } - // Update ReplicationOffset - ReplicationOffset = untilAddress; - logger?.LogInformation("Completed full AOF log replay of {count} records", count); + return untilAddress; } catch (Exception ex) { @@ -144,6 +144,8 @@ private unsafe void RecoverReplay(ref GarnetDatabase db, long untilAddress) output.MemoryOwner?.Dispose(); respServerSession.Dispose(); } + + return -1; } internal unsafe void ProcessAofRecord(IMemoryOwner entry, int length, bool asReplica = false) diff --git a/libs/server/Databases/DatabaseManagerBase.cs b/libs/server/Databases/DatabaseManagerBase.cs index 6783497865..2f11fb9fff 100644 --- a/libs/server/Databases/DatabaseManagerBase.cs +++ b/libs/server/Databases/DatabaseManagerBase.cs @@ -98,7 +98,7 @@ public abstract Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLimit, public abstract long ReplayAOF(long untilAddress = -1); /// - public abstract void DoCompaction(CancellationToken token = default); + public abstract void DoCompaction(CancellationToken token = default, ILogger logger = null); /// public abstract bool GrowIndexesIfNeeded(CancellationToken token = default); @@ -109,9 +109,15 @@ public abstract Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLimit, /// public abstract void Reset(int dbId = 0); + /// + public abstract void ResetRevivificationStats(); + /// public abstract void EnqueueCommit(bool isMainStore, long version, int dbId = 0); + /// + public abstract GarnetDatabase[] GetDatabasesSnapshot(); + /// public abstract bool TryGetDatabase(int dbId, out GarnetDatabase db); @@ -301,9 +307,8 @@ protected long ReplayDatabaseAOF(AofProcessor aofProcessor, ref GarnetDatabase d long replicationOffset = 0; try { - aofProcessor.Recover(ref db, untilAddress); + replicationOffset = aofProcessor.Recover(ref db, untilAddress); db.LastSaveTime = DateTimeOffset.UtcNow; - replicationOffset = aofProcessor.ReplicationOffset; } catch (Exception ex) { @@ -354,14 +359,24 @@ protected void FlushDatabase(ref GarnetDatabase db, bool unsafeTruncateLog) db.ObjectStore?.Log.ShiftBeginAddress(db.ObjectStore.Log.TailAddress, truncateLog: unsafeTruncateLog); } - protected void DoCompaction(ref GarnetDatabase db) + protected void DoCompaction(ref GarnetDatabase db, ILogger logger = null) { - // Periodic compaction -> no need to compact before checkpointing - if (StoreWrapper.serverOptions.CompactionFrequencySecs > 0) return; + try + { + // Periodic compaction -> no need to compact before checkpointing + if (StoreWrapper.serverOptions.CompactionFrequencySecs > 0) return; - DoCompaction(ref db, StoreWrapper.serverOptions.CompactionMaxSegments, - StoreWrapper.serverOptions.ObjectStoreCompactionMaxSegments, 1, - StoreWrapper.serverOptions.CompactionType, StoreWrapper.serverOptions.CompactionForceDelete); + DoCompaction(ref db, StoreWrapper.serverOptions.CompactionMaxSegments, + StoreWrapper.serverOptions.ObjectStoreCompactionMaxSegments, 1, + StoreWrapper.serverOptions.CompactionType, StoreWrapper.serverOptions.CompactionForceDelete); + } + catch (Exception ex) + { + logger?.LogError(ex, + "Exception raised during compaction. AOF tail address = {tailAddress}; AOF committed until address = {commitAddress}; ", + db.AppendOnlyFile.TailAddress, db.AppendOnlyFile.CommittedUntilAddress); + throw; + } } private void DoCompaction(ref GarnetDatabase db, int mainStoreMaxSegments, int objectStoreMaxSegments, int numSegmentsToCompact, LogCompactionType compactionType, bool compactionForceDelete) diff --git a/libs/server/Databases/IDatabaseManager.cs b/libs/server/Databases/IDatabaseManager.cs index a0196b0cc0..a0a6156713 100644 --- a/libs/server/Databases/IDatabaseManager.cs +++ b/libs/server/Databases/IDatabaseManager.cs @@ -158,7 +158,7 @@ public Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLimit, Cancellati /// /// Do compaction /// - public void DoCompaction(CancellationToken token = default); + public void DoCompaction(CancellationToken token = default, ILogger logger = null); /// /// Grows indexes of both main store and object store for all active databases if current size is too small @@ -177,6 +177,11 @@ public Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLimit, Cancellati /// Database ID public void Reset(int dbId = 0); + /// + /// Resets the revivification stats. + /// + public void ResetRevivificationStats(); + /// /// Append a checkpoint commit to the AOF /// @@ -185,6 +190,12 @@ public Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLimit, Cancellati /// public void EnqueueCommit(bool isMainStore, long version, int dbId = 0); + /// + /// Get a snapshot of all active databases + /// + /// Array of active databases + public GarnetDatabase[] GetDatabasesSnapshot(); + /// /// Get database DB ID /// diff --git a/libs/server/Databases/MultiDatabaseManager.cs b/libs/server/Databases/MultiDatabaseManager.cs index 28428d46e4..3710bce9d1 100644 --- a/libs/server/Databases/MultiDatabaseManager.cs +++ b/libs/server/Databases/MultiDatabaseManager.cs @@ -418,7 +418,7 @@ public override long ReplayAOF(long untilAddress = -1) } /// - public override void DoCompaction(CancellationToken token = default) + public override void DoCompaction(CancellationToken token = default, ILogger logger = null) { var lockAcquired = TryGetDatabasesReadLockAsync(token).Result; if (!lockAcquired) return; @@ -430,14 +430,25 @@ public override void DoCompaction(CancellationToken token = default) var activeDbIdsSize = activeDbIdsLength; var activeDbIdsSnapshot = activeDbIds; + var exThrown = false; for (var i = 0; i < activeDbIdsSize; i++) { var dbId = activeDbIdsSnapshot[i]; var db = databasesMapSnapshot[dbId]; Debug.Assert(!db.IsDefault()); - DoCompaction(ref db); + try + { + DoCompaction(ref db); + } + catch (Exception) + { + exThrown = true; + } } + + if (exThrown) + throw new GarnetException($"Error occurred during compaction in {nameof(MultiDatabaseManager)}. Refer to previous log messages for more details."); } finally { @@ -514,6 +525,21 @@ public override void Reset(int dbId = 0) ResetDatabase(ref db); } + /// + public override void ResetRevivificationStats() + { + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + var databaseMapSnapshot = databases.Map; + + for (var i = 0; i < activeDbIdsSize; i++) + { + var dbId = activeDbIdsSnapshot[i]; + databaseMapSnapshot[dbId].MainStore.ResetRevivificationStats(); + databaseMapSnapshot[dbId].ObjectStore?.ResetRevivificationStats(); + } + } + public override void EnqueueCommit(bool isMainStore, long version, int dbId = 0) { if (!TryGetOrAddDatabase(dbId, out var db)) @@ -522,6 +548,23 @@ public override void EnqueueCommit(bool isMainStore, long version, int dbId = 0) EnqueueDatabaseCommit(ref db, isMainStore, version); } + /// + public override GarnetDatabase[] GetDatabasesSnapshot() + { + var activeDbIdsSize = activeDbIdsLength; + var activeDbIdsSnapshot = activeDbIds; + var databaseMapSnapshot = databases.Map; + var databasesSnapshot = new GarnetDatabase[activeDbIdsSize]; + + for (var i = 0; i < activeDbIdsSize; i++) + { + var dbId = activeDbIdsSnapshot[i]; + databasesSnapshot[i] = databaseMapSnapshot[dbId]; + } + + return databasesSnapshot; + } + /// public override bool TrySwapDatabases(int dbId1, int dbId2) { diff --git a/libs/server/Databases/SingleDatabaseManager.cs b/libs/server/Databases/SingleDatabaseManager.cs index 1cf73646ba..8d567536e1 100644 --- a/libs/server/Databases/SingleDatabaseManager.cs +++ b/libs/server/Databases/SingleDatabaseManager.cs @@ -259,7 +259,7 @@ public override long ReplayAOF(long untilAddress = -1) } /// - public override void DoCompaction(CancellationToken token = default) => DoCompaction(ref DefaultDatabase); + public override void DoCompaction(CancellationToken token = default, ILogger logger = null) => DoCompaction(ref DefaultDatabase); public override bool GrowIndexesIfNeeded(CancellationToken token = default) => GrowIndexesIfNeeded(ref DefaultDatabase); @@ -276,6 +276,13 @@ public override void Reset(int dbId = 0) ResetDatabase(ref DefaultDatabase); } + /// + public override void ResetRevivificationStats() + { + MainStore.ResetRevivificationStats(); + ObjectStore?.ResetRevivificationStats(); + } + /// public override void EnqueueCommit(bool isMainStore, long version, int dbId = 0) { @@ -284,6 +291,8 @@ public override void EnqueueCommit(bool isMainStore, long version, int dbId = 0) EnqueueDatabaseCommit(ref DefaultDatabase, isMainStore, version); } + public override GarnetDatabase[] GetDatabasesSnapshot() => [DefaultDatabase]; + /// public override bool TryGetDatabase(int dbId, out GarnetDatabase db) { diff --git a/libs/server/Metrics/GarnetServerMonitor.cs b/libs/server/Metrics/GarnetServerMonitor.cs index 3ab5ab2c23..7133552191 100644 --- a/libs/server/Metrics/GarnetServerMonitor.cs +++ b/libs/server/Metrics/GarnetServerMonitor.cs @@ -193,8 +193,7 @@ private void ResetStats() storeWrapper.clusterProvider?.ResetGossipStats(); - storeWrapper.store.ResetRevivificationStats(); - storeWrapper.objectStore?.ResetRevivificationStats(); + storeWrapper.databaseManager.ResetRevivificationStats(); resetEventFlags[InfoMetricsType.STATS] = false; } diff --git a/libs/server/Metrics/Info/GarnetInfoMetrics.cs b/libs/server/Metrics/Info/GarnetInfoMetrics.cs index 0e1d2f79cf..2ed164aa6d 100644 --- a/libs/server/Metrics/Info/GarnetInfoMetrics.cs +++ b/libs/server/Metrics/Info/GarnetInfoMetrics.cs @@ -63,9 +63,10 @@ private void PopulateServerInfo(StoreWrapper storeWrapper) private void PopulateMemoryInfo(StoreWrapper storeWrapper) { - var main_store_index_size = storeWrapper.store.IndexSize * 64; - var main_store_log_memory_size = storeWrapper.store.Log.MemorySizeBytes; - var main_store_read_cache_size = (storeWrapper.store.ReadCache != null ? storeWrapper.store.ReadCache.MemorySizeBytes : 0); + var dbCount = storeWrapper.DatabaseCount; + var main_store_index_size = storeWrapper.store.IndexSize * 64 * dbCount; + var main_store_log_memory_size = storeWrapper.store.Log.MemorySizeBytes * dbCount; + var main_store_read_cache_size = storeWrapper.store.ReadCache?.MemorySizeBytes * dbCount ?? 0; var total_main_store_size = main_store_index_size + main_store_log_memory_size + main_store_read_cache_size; var object_store_index_size = -1L; @@ -76,16 +77,18 @@ private void PopulateMemoryInfo(StoreWrapper storeWrapper) var total_object_store_size = -1L; var disableObj = storeWrapper.serverOptions.DisableObjects; - var aof_log_memory_size = storeWrapper.appendOnlyFile?.MemorySizeBytes ?? -1; + var aof_log_memory_size = storeWrapper.appendOnlyFile?.MemorySizeBytes * dbCount ?? - 1; if (!disableObj) { - object_store_index_size = storeWrapper.objectStore.IndexSize * 64; - object_store_log_memory_size = storeWrapper.objectStore.Log.MemorySizeBytes; - object_store_read_cache_log_memory_size = storeWrapper.objectStore.ReadCache?.MemorySizeBytes ?? 0; - object_store_heap_memory_size = storeWrapper.objectStoreSizeTracker?.mainLogTracker.LogHeapSizeBytes ?? 0; - object_store_read_cache_heap_memory_size = storeWrapper.objectStoreSizeTracker?.readCacheTracker?.LogHeapSizeBytes ?? 0; - total_object_store_size = object_store_index_size + object_store_log_memory_size + object_store_read_cache_log_memory_size + object_store_heap_memory_size + object_store_read_cache_heap_memory_size; + object_store_index_size = storeWrapper.objectStore.IndexSize * 64 * dbCount; + object_store_log_memory_size = storeWrapper.objectStore.Log.MemorySizeBytes * dbCount; + object_store_read_cache_log_memory_size = storeWrapper.objectStore.ReadCache?.MemorySizeBytes * dbCount ?? 0; + object_store_heap_memory_size = storeWrapper.objectStoreSizeTracker?.mainLogTracker.LogHeapSizeBytes * dbCount ?? 0; + object_store_read_cache_heap_memory_size = storeWrapper.objectStoreSizeTracker?.readCacheTracker?.LogHeapSizeBytes * dbCount ?? 0; + total_object_store_size = object_store_index_size + object_store_log_memory_size + + object_store_read_cache_log_memory_size + object_store_heap_memory_size + + object_store_read_cache_heap_memory_size; } var gcMemoryInfo = GC.GetGCMemoryInfo(); @@ -203,58 +206,80 @@ private void PopulateStatsInfo(StoreWrapper storeWrapper) private void PopulateStoreStats(StoreWrapper storeWrapper) { - storeInfo = - [ - new("CurrentVersion", storeWrapper.store.CurrentVersion.ToString()), - new("LastCheckpointedVersion", storeWrapper.store.LastCheckpointedVersion.ToString()), - new("RecoveredVersion", storeWrapper.store.RecoveredVersion.ToString()), - new("SystemState", storeWrapper.store.SystemState.ToString()), - new("IndexSize", storeWrapper.store.IndexSize.ToString()), - new("LogDir", storeWrapper.serverOptions.LogDir), - new("Log.BeginAddress", storeWrapper.store.Log.BeginAddress.ToString()), - new("Log.BufferSize", storeWrapper.store.Log.BufferSize.ToString()), - new("Log.EmptyPageCount", storeWrapper.store.Log.EmptyPageCount.ToString()), - new("Log.FixedRecordSize", storeWrapper.store.Log.FixedRecordSize.ToString()), - new("Log.HeadAddress", storeWrapper.store.Log.HeadAddress.ToString()), - new("Log.MemorySizeBytes", storeWrapper.store.Log.MemorySizeBytes.ToString()), - new("Log.SafeReadOnlyAddress", storeWrapper.store.Log.SafeReadOnlyAddress.ToString()), - new("Log.TailAddress", storeWrapper.store.Log.TailAddress.ToString()), - new("ReadCache.BeginAddress", storeWrapper.store.ReadCache?.BeginAddress.ToString() ?? "N/A"), - new("ReadCache.BufferSize", storeWrapper.store.ReadCache?.BufferSize.ToString() ?? "N/A"), - new("ReadCache.EmptyPageCount", storeWrapper.store.ReadCache?.EmptyPageCount.ToString() ?? "N/A"), - new("ReadCache.HeadAddress", storeWrapper.store.ReadCache?.HeadAddress.ToString() ?? "N/A"), - new("ReadCache.MemorySizeBytes", storeWrapper.store.ReadCache?.MemorySizeBytes.ToString() ?? "N/A"), - new("ReadCache.TailAddress", storeWrapper.store.ReadCache?.TailAddress.ToString() ?? "N/A"), - ]; + var databases = storeWrapper.databaseManager.GetDatabasesSnapshot(); + + for (var i = 0; i < databases.Length; i++) + { + var storeStats = GetDatabaseStoreStats(storeWrapper, ref databases[i]); + if (i == 0) + storeInfo = new MetricsItem[databases.Length * storeStats.Length]; + + Array.Copy(storeStats, 0, storeInfo, storeStats.Length * i, storeStats.Length); + } } + private MetricsItem[] GetDatabaseStoreStats(StoreWrapper storeWrapper, ref GarnetDatabase db) => + [ + new($"db{db.Id}:CurrentVersion", db.MainStore.CurrentVersion.ToString()), + new($"db{db.Id}:LastCheckpointedVersion", db.MainStore.LastCheckpointedVersion.ToString()), + new($"db{db.Id}:RecoveredVersion", db.MainStore.RecoveredVersion.ToString()), + new($"db{db.Id}:SystemState", db.MainStore.SystemState.ToString()), + new($"db{db.Id}:IndexSize", db.MainStore.IndexSize.ToString()), + new($"db{db.Id}:LogDir", storeWrapper.serverOptions.LogDir), + new($"db{db.Id}:Log.BeginAddress", db.MainStore.Log.BeginAddress.ToString()), + new($"db{db.Id}:Log.BufferSize", db.MainStore.Log.BufferSize.ToString()), + new($"db{db.Id}:Log.EmptyPageCount", db.MainStore.Log.EmptyPageCount.ToString()), + new($"db{db.Id}:Log.FixedRecordSize", db.MainStore.Log.FixedRecordSize.ToString()), + new($"db{db.Id}:Log.HeadAddress", db.MainStore.Log.HeadAddress.ToString()), + new($"db{db.Id}:Log.MemorySizeBytes", db.MainStore.Log.MemorySizeBytes.ToString()), + new($"db{db.Id}:Log.SafeReadOnlyAddress", db.MainStore.Log.SafeReadOnlyAddress.ToString()), + new($"db{db.Id}:Log.TailAddress", db.MainStore.Log.TailAddress.ToString()), + new($"db{db.Id}:ReadCache.BeginAddress", db.MainStore.ReadCache?.BeginAddress.ToString() ?? "N/A"), + new($"db{db.Id}:ReadCache.BufferSize", db.MainStore.ReadCache?.BufferSize.ToString() ?? "N/A"), + new($"db{db.Id}:ReadCache.EmptyPageCount", db.MainStore.ReadCache?.EmptyPageCount.ToString() ?? "N/A"), + new($"db{db.Id}:ReadCache.HeadAddress", db.MainStore.ReadCache?.HeadAddress.ToString() ?? "N/A"), + new($"db{db.Id}:ReadCache.MemorySizeBytes", db.MainStore.ReadCache?.MemorySizeBytes.ToString() ?? "N/A"), + new($"db{db.Id}:ReadCache.TailAddress", db.MainStore.ReadCache?.TailAddress.ToString() ?? "N/A"), + ]; + private void PopulateObjectStoreStats(StoreWrapper storeWrapper) { - objectStoreInfo = - [ - new("CurrentVersion", storeWrapper.objectStore.CurrentVersion.ToString()), - new("LastCheckpointedVersion", storeWrapper.objectStore.LastCheckpointedVersion.ToString()), - new("RecoveredVersion", storeWrapper.objectStore.RecoveredVersion.ToString()), - new("SystemState", storeWrapper.objectStore.SystemState.ToString()), - new("IndexSize", storeWrapper.objectStore.IndexSize.ToString()), - new("LogDir", storeWrapper.serverOptions.LogDir), - new("Log.BeginAddress", storeWrapper.objectStore.Log.BeginAddress.ToString()), - new("Log.BufferSize", storeWrapper.objectStore.Log.BufferSize.ToString()), - new("Log.EmptyPageCount", storeWrapper.objectStore.Log.EmptyPageCount.ToString()), - new("Log.FixedRecordSize", storeWrapper.objectStore.Log.FixedRecordSize.ToString()), - new("Log.HeadAddress", storeWrapper.objectStore.Log.HeadAddress.ToString()), - new("Log.MemorySizeBytes", storeWrapper.objectStore.Log.MemorySizeBytes.ToString()), - new("Log.SafeReadOnlyAddress", storeWrapper.objectStore.Log.SafeReadOnlyAddress.ToString()), - new("Log.TailAddress", storeWrapper.objectStore.Log.TailAddress.ToString()), - new("ReadCache.BeginAddress", storeWrapper.objectStore.ReadCache?.BeginAddress.ToString() ?? "N/A"), - new("ReadCache.BufferSize", storeWrapper.objectStore.ReadCache?.BufferSize.ToString() ?? "N/A"), - new("ReadCache.EmptyPageCount", storeWrapper.objectStore.ReadCache?.EmptyPageCount.ToString() ?? "N/A"), - new("ReadCache.HeadAddress", storeWrapper.objectStore.ReadCache?.HeadAddress.ToString() ?? "N/A"), - new("ReadCache.MemorySizeBytes", storeWrapper.objectStore.ReadCache?.MemorySizeBytes.ToString() ?? "N/A"), - new("ReadCache.TailAddress", storeWrapper.objectStore.ReadCache?.TailAddress.ToString() ?? "N/A"), - ]; + var databases = storeWrapper.databaseManager.GetDatabasesSnapshot(); + + for (var i = 0; i < databases.Length; i++) + { + var storeStats = GetDatabaseObjectStoreStats(storeWrapper, ref databases[i]); + if (i == 0) + objectStoreInfo = new MetricsItem[databases.Length * storeStats.Length]; + + Array.Copy(storeStats, 0, objectStoreInfo, storeStats.Length * i, storeStats.Length); + } } + private MetricsItem[] GetDatabaseObjectStoreStats(StoreWrapper storeWrapper, ref GarnetDatabase db) => + [ + new($"db{db.Id}:CurrentVersion", db.ObjectStore.CurrentVersion.ToString()), + new($"db{db.Id}:LastCheckpointedVersion", db.ObjectStore.LastCheckpointedVersion.ToString()), + new($"db{db.Id}:RecoveredVersion", db.ObjectStore.RecoveredVersion.ToString()), + new($"db{db.Id}:SystemState", db.ObjectStore.SystemState.ToString()), + new($"db{db.Id}:IndexSize", db.ObjectStore.IndexSize.ToString()), + new($"db{db.Id}:LogDir", storeWrapper.serverOptions.LogDir), + new($"db{db.Id}:Log.BeginAddress", db.ObjectStore.Log.BeginAddress.ToString()), + new($"db{db.Id}:Log.BufferSize", db.ObjectStore.Log.BufferSize.ToString()), + new($"db{db.Id}:Log.EmptyPageCount", db.ObjectStore.Log.EmptyPageCount.ToString()), + new($"db{db.Id}:Log.FixedRecordSize", db.ObjectStore.Log.FixedRecordSize.ToString()), + new($"db{db.Id}:Log.HeadAddress", db.ObjectStore.Log.HeadAddress.ToString()), + new($"db{db.Id}:Log.MemorySizeBytes", db.ObjectStore.Log.MemorySizeBytes.ToString()), + new($"db{db.Id}:Log.SafeReadOnlyAddress", db.ObjectStore.Log.SafeReadOnlyAddress.ToString()), + new($"db{db.Id}:Log.TailAddress", db.ObjectStore.Log.TailAddress.ToString()), + new($"db{db.Id}:ReadCache.BeginAddress", db.ObjectStore.ReadCache?.BeginAddress.ToString() ?? "N/A"), + new($"db{db.Id}:ReadCache.BufferSize", db.ObjectStore.ReadCache?.BufferSize.ToString() ?? "N/A"), + new($"db{db.Id}:ReadCache.EmptyPageCount", db.ObjectStore.ReadCache?.EmptyPageCount.ToString() ?? "N/A"), + new($"db{db.Id}:ReadCache.HeadAddress", db.ObjectStore.ReadCache?.HeadAddress.ToString() ?? "N/A"), + new($"db{db.Id}:ReadCache.MemorySizeBytes", db.ObjectStore.ReadCache?.MemorySizeBytes.ToString() ?? "N/A"), + new($"db{db.Id}:ReadCache.TailAddress", db.ObjectStore.ReadCache?.TailAddress.ToString() ?? "N/A"), + ]; + private void PopulateStoreHashDistribution(StoreWrapper storeWrapper) => storeHashDistrInfo = [new("", storeWrapper.store.DumpDistribution())]; private void PopulateObjectStoreHashDistribution(StoreWrapper storeWrapper) => objectStoreHashDistrInfo = [new("", storeWrapper.objectStore.DumpDistribution())]; diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index 74915da7f5..5b817d93c0 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -100,8 +100,7 @@ internal sealed unsafe partial class RespServerSession : ServerSessionBase internal int activeDbId; readonly bool allowMultiDb; - readonly int maxDbs; - ExpandableMap databaseSessions; + internal ExpandableMap databaseSessions; /// /// The user currently authenticated in this session @@ -227,9 +226,9 @@ public RespServerSession( sessionScriptCache = new(storeWrapper, _authenticator, logger); var dbSession = CreateDatabaseSession(0); - maxDbs = storeWrapper.serverOptions.MaxDatabases; + var maxDbs = storeWrapper.serverOptions.MaxDatabases; activeDbId = 0; - allowMultiDb = maxDbs > 1; + allowMultiDb = !storeWrapper.serverOptions.EnableCluster && maxDbs > 1; if (allowMultiDb) { diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index f9fb141909..e3bbb6c506 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -113,6 +113,11 @@ public sealed class StoreWrapper /// public delegate GarnetDatabase DatabaseCreatorDelegate(int dbId, out string storeCheckpointDir, out string aofDir); + /// + /// Number of active databases + /// + public int DatabaseCount => databaseManager.DatabaseCount; + internal readonly IDatabaseManager databaseManager; internal readonly CollectionItemBroker itemBroker; @@ -383,7 +388,7 @@ async Task CompactionTask(int compactionFrequencySecs, CancellationToken token = { if (token.IsCancellationRequested) return; - databaseManager.DoCompaction(token); + databaseManager.DoCompaction(token, logger); if (!serverOptions.CompactionForceDelete) logger?.LogInformation("NOTE: Take a checkpoint (SAVE/BGSAVE) in order to actually delete the older data segments (files) from disk"); @@ -395,7 +400,7 @@ async Task CompactionTask(int compactionFrequencySecs, CancellationToken token = } catch (Exception ex) { - logger?.LogError(ex, "CompactionTask exception received, AOF tail address = {tailAddress}; AOF committed until address = {commitAddress}; ", appendOnlyFile.TailAddress, appendOnlyFile.CommittedUntilAddress); + logger?.LogError(ex, "CompactionTask exception received."); } } @@ -407,7 +412,7 @@ async Task HashCollectTask(int hashCollectFrequencySecs, CancellationToken token var scratchBufferManager = new ScratchBufferManager(); using var storageSession = new StorageSession(this, scratchBufferManager, null, null, logger); - if (objectStore is null) + if (serverOptions.DisableObjects) { logger?.LogWarning("HashCollectFrequencySecs option is configured but Object store is disabled. Stopping the background hash collect task."); return; @@ -448,7 +453,7 @@ static void ExecuteHashCollect(ScratchBufferManager scratchBufferManager, Storag /// True if should wait until all commits complete internal void CommitAOF(bool spinWait) { - if (!serverOptions.EnableAOF || appendOnlyFile == null) return; + if (!serverOptions.EnableAOF) return; var task = databaseManager.CommitToAofAsync(); if (!spinWait) return; @@ -469,7 +474,7 @@ internal void WaitForCommit() => /// ValueTask internal async ValueTask WaitForCommitAsync(CancellationToken token = default) { - if (!serverOptions.EnableAOF || appendOnlyFile == null) return; + if (!serverOptions.EnableAOF) return; await databaseManager.WaitForCommitToAofAsync(token); } @@ -483,7 +488,7 @@ internal async ValueTask WaitForCommitAsync(CancellationToken token = default) /// ValueTask internal async ValueTask CommitAOFAsync(int dbId = -1, CancellationToken token = default) { - if (!serverOptions.EnableAOF || appendOnlyFile == null) return; + if (!serverOptions.EnableAOF) return; if (dbId == -1) { @@ -524,7 +529,7 @@ internal void Start() Task.Run(async () => await AutoCheckpointBasedOnAofSizeLimit(aofSizeLimitBytes, ctsCommit.Token, logger)); } - if (serverOptions.CommitFrequencyMs > 0 && appendOnlyFile != null) + if (serverOptions.CommitFrequencyMs > 0 && serverOptions.EnableAOF) { Task.Run(async () => await CommitTask(serverOptions.CommitFrequencyMs, logger, ctsCommit.Token)); } @@ -629,7 +634,7 @@ public bool HasKeysInSlots(List slots) } } - if (!hasKeyInSlots && objectStore != null) + if (!hasKeyInSlots && !serverOptions.DisableObjects) { var functionsState = databaseManager.CreateFunctionsState(); var objstorefunctions = new ObjectSessionFunctions(functionsState); From bc1db5f561b687a4b6e34bc6d12ad82f5ef843ff Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 4 Mar 2025 17:04:27 -0800 Subject: [PATCH 66/82] wip - tests passing --- libs/server/Metrics/Info/GarnetInfoMetrics.cs | 80 +++++++++---------- libs/server/Resp/Parser/RespCommand.cs | 2 +- libs/server/Resp/RespServerSession.cs | 9 +-- test/Garnet.test.cluster/ClusterTestUtils.cs | 2 +- test/Garnet.test/Resp/ACL/RespCommandTests.cs | 15 ++++ test/Garnet.test/TestUtils.cs | 16 ++-- 6 files changed, 68 insertions(+), 56 deletions(-) diff --git a/libs/server/Metrics/Info/GarnetInfoMetrics.cs b/libs/server/Metrics/Info/GarnetInfoMetrics.cs index 2ed164aa6d..9d8006873e 100644 --- a/libs/server/Metrics/Info/GarnetInfoMetrics.cs +++ b/libs/server/Metrics/Info/GarnetInfoMetrics.cs @@ -220,26 +220,26 @@ private void PopulateStoreStats(StoreWrapper storeWrapper) private MetricsItem[] GetDatabaseStoreStats(StoreWrapper storeWrapper, ref GarnetDatabase db) => [ - new($"db{db.Id}:CurrentVersion", db.MainStore.CurrentVersion.ToString()), - new($"db{db.Id}:LastCheckpointedVersion", db.MainStore.LastCheckpointedVersion.ToString()), - new($"db{db.Id}:RecoveredVersion", db.MainStore.RecoveredVersion.ToString()), - new($"db{db.Id}:SystemState", db.MainStore.SystemState.ToString()), - new($"db{db.Id}:IndexSize", db.MainStore.IndexSize.ToString()), - new($"db{db.Id}:LogDir", storeWrapper.serverOptions.LogDir), - new($"db{db.Id}:Log.BeginAddress", db.MainStore.Log.BeginAddress.ToString()), - new($"db{db.Id}:Log.BufferSize", db.MainStore.Log.BufferSize.ToString()), - new($"db{db.Id}:Log.EmptyPageCount", db.MainStore.Log.EmptyPageCount.ToString()), - new($"db{db.Id}:Log.FixedRecordSize", db.MainStore.Log.FixedRecordSize.ToString()), - new($"db{db.Id}:Log.HeadAddress", db.MainStore.Log.HeadAddress.ToString()), - new($"db{db.Id}:Log.MemorySizeBytes", db.MainStore.Log.MemorySizeBytes.ToString()), - new($"db{db.Id}:Log.SafeReadOnlyAddress", db.MainStore.Log.SafeReadOnlyAddress.ToString()), - new($"db{db.Id}:Log.TailAddress", db.MainStore.Log.TailAddress.ToString()), - new($"db{db.Id}:ReadCache.BeginAddress", db.MainStore.ReadCache?.BeginAddress.ToString() ?? "N/A"), - new($"db{db.Id}:ReadCache.BufferSize", db.MainStore.ReadCache?.BufferSize.ToString() ?? "N/A"), - new($"db{db.Id}:ReadCache.EmptyPageCount", db.MainStore.ReadCache?.EmptyPageCount.ToString() ?? "N/A"), - new($"db{db.Id}:ReadCache.HeadAddress", db.MainStore.ReadCache?.HeadAddress.ToString() ?? "N/A"), - new($"db{db.Id}:ReadCache.MemorySizeBytes", db.MainStore.ReadCache?.MemorySizeBytes.ToString() ?? "N/A"), - new($"db{db.Id}:ReadCache.TailAddress", db.MainStore.ReadCache?.TailAddress.ToString() ?? "N/A"), + new($"db{db.Id}.CurrentVersion", db.MainStore.CurrentVersion.ToString()), + new($"db{db.Id}.LastCheckpointedVersion", db.MainStore.LastCheckpointedVersion.ToString()), + new($"db{db.Id}.RecoveredVersion", db.MainStore.RecoveredVersion.ToString()), + new($"db{db.Id}.SystemState", db.MainStore.SystemState.ToString()), + new($"db{db.Id}.IndexSize", db.MainStore.IndexSize.ToString()), + new($"db{db.Id}.LogDir", storeWrapper.serverOptions.LogDir), + new($"db{db.Id}.Log.BeginAddress", db.MainStore.Log.BeginAddress.ToString()), + new($"db{db.Id}.Log.BufferSize", db.MainStore.Log.BufferSize.ToString()), + new($"db{db.Id}.Log.EmptyPageCount", db.MainStore.Log.EmptyPageCount.ToString()), + new($"db{db.Id}.Log.FixedRecordSize", db.MainStore.Log.FixedRecordSize.ToString()), + new($"db{db.Id}.Log.HeadAddress", db.MainStore.Log.HeadAddress.ToString()), + new($"db{db.Id}.Log.MemorySizeBytes", db.MainStore.Log.MemorySizeBytes.ToString()), + new($"db{db.Id}.Log.SafeReadOnlyAddress", db.MainStore.Log.SafeReadOnlyAddress.ToString()), + new($"db{db.Id}.Log.TailAddress", db.MainStore.Log.TailAddress.ToString()), + new($"db{db.Id}.ReadCache.BeginAddress", db.MainStore.ReadCache?.BeginAddress.ToString() ?? "N/A"), + new($"db{db.Id}.ReadCache.BufferSize", db.MainStore.ReadCache?.BufferSize.ToString() ?? "N/A"), + new($"db{db.Id}.ReadCache.EmptyPageCount", db.MainStore.ReadCache?.EmptyPageCount.ToString() ?? "N/A"), + new($"db{db.Id}.ReadCache.HeadAddress", db.MainStore.ReadCache?.HeadAddress.ToString() ?? "N/A"), + new($"db{db.Id}.ReadCache.MemorySizeBytes", db.MainStore.ReadCache?.MemorySizeBytes.ToString() ?? "N/A"), + new($"db{db.Id}.ReadCache.TailAddress", db.MainStore.ReadCache?.TailAddress.ToString() ?? "N/A"), ]; private void PopulateObjectStoreStats(StoreWrapper storeWrapper) @@ -258,26 +258,26 @@ private void PopulateObjectStoreStats(StoreWrapper storeWrapper) private MetricsItem[] GetDatabaseObjectStoreStats(StoreWrapper storeWrapper, ref GarnetDatabase db) => [ - new($"db{db.Id}:CurrentVersion", db.ObjectStore.CurrentVersion.ToString()), - new($"db{db.Id}:LastCheckpointedVersion", db.ObjectStore.LastCheckpointedVersion.ToString()), - new($"db{db.Id}:RecoveredVersion", db.ObjectStore.RecoveredVersion.ToString()), - new($"db{db.Id}:SystemState", db.ObjectStore.SystemState.ToString()), - new($"db{db.Id}:IndexSize", db.ObjectStore.IndexSize.ToString()), - new($"db{db.Id}:LogDir", storeWrapper.serverOptions.LogDir), - new($"db{db.Id}:Log.BeginAddress", db.ObjectStore.Log.BeginAddress.ToString()), - new($"db{db.Id}:Log.BufferSize", db.ObjectStore.Log.BufferSize.ToString()), - new($"db{db.Id}:Log.EmptyPageCount", db.ObjectStore.Log.EmptyPageCount.ToString()), - new($"db{db.Id}:Log.FixedRecordSize", db.ObjectStore.Log.FixedRecordSize.ToString()), - new($"db{db.Id}:Log.HeadAddress", db.ObjectStore.Log.HeadAddress.ToString()), - new($"db{db.Id}:Log.MemorySizeBytes", db.ObjectStore.Log.MemorySizeBytes.ToString()), - new($"db{db.Id}:Log.SafeReadOnlyAddress", db.ObjectStore.Log.SafeReadOnlyAddress.ToString()), - new($"db{db.Id}:Log.TailAddress", db.ObjectStore.Log.TailAddress.ToString()), - new($"db{db.Id}:ReadCache.BeginAddress", db.ObjectStore.ReadCache?.BeginAddress.ToString() ?? "N/A"), - new($"db{db.Id}:ReadCache.BufferSize", db.ObjectStore.ReadCache?.BufferSize.ToString() ?? "N/A"), - new($"db{db.Id}:ReadCache.EmptyPageCount", db.ObjectStore.ReadCache?.EmptyPageCount.ToString() ?? "N/A"), - new($"db{db.Id}:ReadCache.HeadAddress", db.ObjectStore.ReadCache?.HeadAddress.ToString() ?? "N/A"), - new($"db{db.Id}:ReadCache.MemorySizeBytes", db.ObjectStore.ReadCache?.MemorySizeBytes.ToString() ?? "N/A"), - new($"db{db.Id}:ReadCache.TailAddress", db.ObjectStore.ReadCache?.TailAddress.ToString() ?? "N/A"), + new($"db{db.Id}.CurrentVersion", db.ObjectStore.CurrentVersion.ToString()), + new($"db{db.Id}.LastCheckpointedVersion", db.ObjectStore.LastCheckpointedVersion.ToString()), + new($"db{db.Id}.RecoveredVersion", db.ObjectStore.RecoveredVersion.ToString()), + new($"db{db.Id}.SystemState", db.ObjectStore.SystemState.ToString()), + new($"db{db.Id}.IndexSize", db.ObjectStore.IndexSize.ToString()), + new($"db{db.Id}.LogDir", storeWrapper.serverOptions.LogDir), + new($"db{db.Id}.Log.BeginAddress", db.ObjectStore.Log.BeginAddress.ToString()), + new($"db{db.Id}.Log.BufferSize", db.ObjectStore.Log.BufferSize.ToString()), + new($"db{db.Id}.Log.EmptyPageCount", db.ObjectStore.Log.EmptyPageCount.ToString()), + new($"db{db.Id}.Log.FixedRecordSize", db.ObjectStore.Log.FixedRecordSize.ToString()), + new($"db{db.Id}.Log.HeadAddress", db.ObjectStore.Log.HeadAddress.ToString()), + new($"db{db.Id}.Log.MemorySizeBytes", db.ObjectStore.Log.MemorySizeBytes.ToString()), + new($"db{db.Id}.Log.SafeReadOnlyAddress", db.ObjectStore.Log.SafeReadOnlyAddress.ToString()), + new($"db{db.Id}.Log.TailAddress", db.ObjectStore.Log.TailAddress.ToString()), + new($"db{db.Id}.ReadCache.BeginAddress", db.ObjectStore.ReadCache?.BeginAddress.ToString() ?? "N/A"), + new($"db{db.Id}.ReadCache.BufferSize", db.ObjectStore.ReadCache?.BufferSize.ToString() ?? "N/A"), + new($"db{db.Id}.ReadCache.EmptyPageCount", db.ObjectStore.ReadCache?.EmptyPageCount.ToString() ?? "N/A"), + new($"db{db.Id}.ReadCache.HeadAddress", db.ObjectStore.ReadCache?.HeadAddress.ToString() ?? "N/A"), + new($"db{db.Id}.ReadCache.MemorySizeBytes", db.ObjectStore.ReadCache?.MemorySizeBytes.ToString() ?? "N/A"), + new($"db{db.Id}.ReadCache.TailAddress", db.ObjectStore.ReadCache?.TailAddress.ToString() ?? "N/A"), ]; private void PopulateStoreHashDistribution(StoreWrapper storeWrapper) => storeHashDistrInfo = [new("", storeWrapper.store.DumpDistribution())]; diff --git a/libs/server/Resp/Parser/RespCommand.cs b/libs/server/Resp/Parser/RespCommand.cs index 61aa0134b0..8bf2f5639a 100644 --- a/libs/server/Resp/Parser/RespCommand.cs +++ b/libs/server/Resp/Parser/RespCommand.cs @@ -182,6 +182,7 @@ public enum RespCommand : ushort SPOP, SREM, SUNIONSTORE, + SWAPDB, UNLINK, ZADD, ZDIFFSTORE, @@ -263,7 +264,6 @@ public enum RespCommand : ushort FORCEGC, PURGEBP, FAILOVER, - SWAPDB, // Custom commands CustomTxn, diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index 5b817d93c0..acebac73f9 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -230,12 +230,9 @@ public RespServerSession( activeDbId = 0; allowMultiDb = !storeWrapper.serverOptions.EnableCluster && maxDbs > 1; - if (allowMultiDb) - { - databaseSessions = new ExpandableMap(1, 0, maxDbs - 1); - if (!databaseSessions.TrySetValue(0, ref dbSession)) - throw new GarnetException("Failed to set initial database session in database sessions map"); - } + databaseSessions = new ExpandableMap(1, 0, maxDbs - 1); + if (!databaseSessions.TrySetValue(0, ref dbSession)) + throw new GarnetException("Failed to set initial database session in database sessions map"); SwitchActiveDatabaseSession(0, ref dbSession); diff --git a/test/Garnet.test.cluster/ClusterTestUtils.cs b/test/Garnet.test.cluster/ClusterTestUtils.cs index bad107c5e4..9964ad43ad 100644 --- a/test/Garnet.test.cluster/ClusterTestUtils.cs +++ b/test/Garnet.test.cluster/ClusterTestUtils.cs @@ -2697,7 +2697,7 @@ public string GetInfo(IPEndPoint endPoint, string section, string segment, ILogg var result = server.Info(section); ClassicAssert.AreEqual(1, result.Length, "section does not exist"); foreach (var item in result[0]) - if (item.Key.Equals(segment)) + if (item.Key.Equals($"db0.{segment}")) return item.Value; Assert.Fail($"Segment not available for {section} section"); return ""; diff --git a/test/Garnet.test/Resp/ACL/RespCommandTests.cs b/test/Garnet.test/Resp/ACL/RespCommandTests.cs index 412d026bcf..10d80f89cb 100644 --- a/test/Garnet.test/Resp/ACL/RespCommandTests.cs +++ b/test/Garnet.test/Resp/ACL/RespCommandTests.cs @@ -6926,6 +6926,21 @@ async Task DoRestoreAsync(GarnetClient client) } } + [Test] + public async Task SwapDbACLsAsync() + { + await CheckCommandsAsync( + "SWAPDB", + [DoSwapDbAsync] + ); + + static async Task DoSwapDbAsync(GarnetClient client) + { + string val = await client.ExecuteForStringResultAsync("SWAPDB", ["1", "0"]); + ClassicAssert.AreEqual("OK", val); + } + } + [Test] public async Task TypeACLsAsync() { diff --git a/test/Garnet.test/TestUtils.cs b/test/Garnet.test/TestUtils.cs index 343540c21d..bc23774641 100644 --- a/test/Garnet.test/TestUtils.cs +++ b/test/Garnet.test/TestUtils.cs @@ -913,7 +913,7 @@ public static void CreateTestLibrary(string[] namespaces, string[] referenceFile } } - public static StoreAddressInfo GetStoreAddressInfo(IServer server, bool includeReadCache = false, bool isObjectStore = false) + public static StoreAddressInfo GetStoreAddressInfo(IServer server, int dbId = 0, bool includeReadCache = false, bool isObjectStore = false) { StoreAddressInfo result = default; var info = isObjectStore ? server.Info("OBJECTSTORE") : server.Info("STORE"); @@ -921,19 +921,19 @@ public static StoreAddressInfo GetStoreAddressInfo(IServer server, bool includeR { foreach (var entry in section) { - if (entry.Key.Equals("Log.BeginAddress")) + if (entry.Key.Equals($"db{dbId}.Log.BeginAddress")) result.BeginAddress = long.Parse(entry.Value); - else if (entry.Key.Equals("Log.HeadAddress")) + else if (entry.Key.Equals($"db{dbId}.Log.HeadAddress")) result.HeadAddress = long.Parse(entry.Value); - else if (entry.Key.Equals("Log.SafeReadOnlyAddress")) + else if (entry.Key.Equals($"db{dbId}.Log.SafeReadOnlyAddress")) result.ReadOnlyAddress = long.Parse(entry.Value); - else if (entry.Key.Equals("Log.TailAddress")) + else if (entry.Key.Equals($"db{dbId}.Log.TailAddress")) result.TailAddress = long.Parse(entry.Value); - else if (entry.Key.Equals("Log.MemorySizeBytes")) + else if (entry.Key.Equals($"db{dbId}.Log.MemorySizeBytes")) result.MemorySize = long.Parse(entry.Value); - else if (includeReadCache && entry.Key.Equals("ReadCache.BeginAddress")) + else if (includeReadCache && entry.Key.Equals($"db{dbId}.ReadCache.BeginAddress")) result.ReadCacheBeginAddress = long.Parse(entry.Value); - else if (includeReadCache && entry.Key.Equals("ReadCache.TailAddress")) + else if (includeReadCache && entry.Key.Equals($"db{dbId}.ReadCache.TailAddress")) result.ReadCacheTailAddress = long.Parse(entry.Value); } } From 32d5c06de33f06f34228943f74028644242654ec Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 4 Mar 2025 17:16:17 -0800 Subject: [PATCH 67/82] wip --- libs/host/GarnetServer.cs | 7 ++-- .../Databases/DatabaseManagerFactory.cs | 29 ++++++++++++++++ libs/server/StoreWrapper.cs | 34 ++++++++----------- 3 files changed, 49 insertions(+), 21 deletions(-) create mode 100644 libs/server/Databases/DatabaseManagerFactory.cs diff --git a/libs/host/GarnetServer.cs b/libs/host/GarnetServer.cs index b417f784c2..7e5599df48 100644 --- a/libs/host/GarnetServer.cs +++ b/libs/host/GarnetServer.cs @@ -241,8 +241,11 @@ private void InitializeServer() this.server ??= new GarnetServerTcp(opts.EndPoint, 0, opts.TlsOptions, opts.NetworkSendThrottleMax, opts.NetworkConnectionLimit, opts.UnixSocketPath, opts.UnixSocketPermission, logger); - storeWrapper = new StoreWrapper(version, redisProtocolVersion, server, createDatabaseDelegate, - customCommandManager, opts, subscribeBroker, clusterFactory: clusterFactory, loggerFactory: loggerFactory); + storeWrapper = new StoreWrapper(version, redisProtocolVersion, server, + customCommandManager, opts, subscribeBroker, + createDatabaseDelegate: createDatabaseDelegate, + clusterFactory: clusterFactory, + loggerFactory: loggerFactory); if (logger != null) { diff --git a/libs/server/Databases/DatabaseManagerFactory.cs b/libs/server/Databases/DatabaseManagerFactory.cs new file mode 100644 index 0000000000..377ee9cba3 --- /dev/null +++ b/libs/server/Databases/DatabaseManagerFactory.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using static Garnet.server.StoreWrapper; + +namespace Garnet.server.Databases +{ + /// + /// Factory class for creating new instances of IDatabaseManager + /// + public class DatabaseManagerFactory + { + /// + /// Create a new instance of IDatabaseManager + /// + /// Garnet server options + /// Delegate for creating a new logical database + /// Store wrapper instance + /// True if database manager should create a default database instance (default: true) + /// + public static IDatabaseManager CreateDatabaseManager(GarnetServerOptions serverOptions, + DatabaseCreatorDelegate createDatabaseDelegate, StoreWrapper storeWrapper, bool createDefaultDatabase = true) + { + return serverOptions.EnableCluster || serverOptions.MaxDatabases == 1 + ? new SingleDatabaseManager(createDatabaseDelegate, storeWrapper, createDefaultDatabase) + : new MultiDatabaseManager(createDatabaseDelegate, storeWrapper, createDefaultDatabase); + } + } +} diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index d7dd88c5fa..6cfe6e74ac 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -12,6 +12,7 @@ using Garnet.common; using Garnet.server.ACL; using Garnet.server.Auth.Settings; +using Garnet.server.Databases; using Microsoft.Extensions.Logging; using Tsavorite.core; @@ -131,8 +132,6 @@ public sealed class StoreWrapper // Standalone instance node_id internal readonly string runId; - private readonly DatabaseCreatorDelegate createDatabaseDelegate; - readonly CancellationTokenSource ctsCommit; // True if StoreWrapper instance is disposed @@ -145,11 +144,12 @@ public StoreWrapper( string version, string redisProtocolVersion, IGarnetServer server, - DatabaseCreatorDelegate createDatabaseDelegate, CustomCommandManager customCommandManager, GarnetServerOptions serverOptions, SubscribeBroker subscribeBroker, AccessControlList accessControlList = null, + DatabaseCreatorDelegate createDatabaseDelegate = null, + IDatabaseManager databaseManager = null, IClusterFactory clusterFactory = null, ILoggerFactory loggerFactory = null) { @@ -160,10 +160,7 @@ public StoreWrapper( this.serverOptions = serverOptions; this.subscribeBroker = subscribeBroker; this.customCommandManager = customCommandManager; - this.createDatabaseDelegate = createDatabaseDelegate; - this.databaseManager = serverOptions.EnableCluster || serverOptions.MaxDatabases == 1 - ? new SingleDatabaseManager(createDatabaseDelegate, this) - : new MultiDatabaseManager(createDatabaseDelegate, this); + this.databaseManager = databaseManager ?? DatabaseManagerFactory.CreateDatabaseManager(serverOptions, createDatabaseDelegate, this); this.monitor = serverOptions.MetricsSamplingFrequency > 0 ? new GarnetServerMonitor(this, serverOptions, server, loggerFactory?.CreateLogger("GarnetServerMonitor")) @@ -228,19 +225,18 @@ public StoreWrapper( /// Copy Constructor /// /// Source instance - /// Enable AOF in StoreWrapper copy - public StoreWrapper(StoreWrapper storeWrapper, bool recordToAof) + /// Enable AOF in database manager + public StoreWrapper(StoreWrapper storeWrapper, bool recordToAof) : this(storeWrapper.version, + storeWrapper.redisProtocolVersion, + storeWrapper.server, + storeWrapper.customCommandManager, + storeWrapper.serverOptions, + storeWrapper.subscribeBroker, + storeWrapper.accessControlList, + databaseManager: storeWrapper.databaseManager.Clone(recordToAof), + clusterFactory: null, + loggerFactory: storeWrapper.loggerFactory) { - this.version = storeWrapper.version; - this.redisProtocolVersion = storeWrapper.version; - this.server = storeWrapper.server; - this.startupTime = DateTimeOffset.UtcNow.Ticks; - this.serverOptions = storeWrapper.serverOptions; - this.subscribeBroker = storeWrapper.subscribeBroker; - this.customCommandManager = storeWrapper.customCommandManager; - this.loggerFactory = storeWrapper.loggerFactory; - this.databaseManager = storeWrapper.databaseManager.Clone(recordToAof); - this.accessControlList = storeWrapper.accessControlList; } /// From db7e587d341b640ac41832abcbb5a9a9d59c5f76 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 4 Mar 2025 18:08:56 -0800 Subject: [PATCH 68/82] format --- libs/host/GarnetServer.cs | 4 +- libs/server/Databases/DatabaseManagerBase.cs | 2 +- .../Databases/DatabaseManagerFactory.cs | 2 +- libs/server/Databases/IDatabaseManager.cs | 2 +- libs/server/Databases/MultiDatabaseManager.cs | 10 +- .../server/Databases/SingleDatabaseManager.cs | 10 +- libs/server/Metrics/Info/GarnetInfoMetrics.cs | 150 +++++++++++++----- libs/server/StoreWrapper.cs | 10 +- 8 files changed, 132 insertions(+), 58 deletions(-) diff --git a/libs/host/GarnetServer.cs b/libs/host/GarnetServer.cs index 7e5599df48..46bd13ae77 100644 --- a/libs/host/GarnetServer.cs +++ b/libs/host/GarnetServer.cs @@ -242,9 +242,9 @@ private void InitializeServer() opts.NetworkConnectionLimit, opts.UnixSocketPath, opts.UnixSocketPermission, logger); storeWrapper = new StoreWrapper(version, redisProtocolVersion, server, - customCommandManager, opts, subscribeBroker, + customCommandManager, opts, subscribeBroker, createDatabaseDelegate: createDatabaseDelegate, - clusterFactory: clusterFactory, + clusterFactory: clusterFactory, loggerFactory: loggerFactory); if (logger != null) diff --git a/libs/server/Databases/DatabaseManagerBase.cs b/libs/server/Databases/DatabaseManagerBase.cs index 2f11fb9fff..aa73776123 100644 --- a/libs/server/Databases/DatabaseManagerBase.cs +++ b/libs/server/Databases/DatabaseManagerBase.cs @@ -566,4 +566,4 @@ protected bool GrowIndexIfNeeded(StoreType storeType, long indexMaxSize, long ov return true; } } -} +} \ No newline at end of file diff --git a/libs/server/Databases/DatabaseManagerFactory.cs b/libs/server/Databases/DatabaseManagerFactory.cs index 377ee9cba3..9702df29eb 100644 --- a/libs/server/Databases/DatabaseManagerFactory.cs +++ b/libs/server/Databases/DatabaseManagerFactory.cs @@ -26,4 +26,4 @@ public static IDatabaseManager CreateDatabaseManager(GarnetServerOptions serverO : new MultiDatabaseManager(createDatabaseDelegate, storeWrapper, createDefaultDatabase); } } -} +} \ No newline at end of file diff --git a/libs/server/Databases/IDatabaseManager.cs b/libs/server/Databases/IDatabaseManager.cs index a0a6156713..34d520ea31 100644 --- a/libs/server/Databases/IDatabaseManager.cs +++ b/libs/server/Databases/IDatabaseManager.cs @@ -234,4 +234,4 @@ public Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLimit, Cancellati internal FunctionsState CreateFunctionsState(int dbId = 0); } -} +} \ No newline at end of file diff --git a/libs/server/Databases/MultiDatabaseManager.cs b/libs/server/Databases/MultiDatabaseManager.cs index 3710bce9d1..dd97649661 100644 --- a/libs/server/Databases/MultiDatabaseManager.cs +++ b/libs/server/Databases/MultiDatabaseManager.cs @@ -117,7 +117,7 @@ public override void RecoverCheckpoint(bool replicaRecover = false, bool recover } long storeVersion = -1, objectStoreVersion = -1; - + foreach (var dbId in dbIdsToRecover) { if (!TryGetOrAddDatabase(dbId, out var db)) @@ -250,7 +250,7 @@ public override async Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLi } if (dbIdsIdx == 0) return; - + await TakeDatabasesCheckpointAsync(StoreType.All, dbIdsIdx, logger: logger, token: token); } finally @@ -305,7 +305,7 @@ public override async Task CommitToAofAsync(CancellationToken token = default, I t.Result.Item1, t.Result.Item2); } - if (exThrown) + if (exThrown) throw new GarnetException($"Error occurred while committing to AOF in {nameof(MultiDatabaseManager)}. Refer to previous log messages for more details."); } finally @@ -474,7 +474,7 @@ public override bool GrowIndexesIfNeeded(CancellationToken token = default) for (var i = 0; i < activeDbIdsSize; i++) { var dbId = activeDbIdsSnapshot[i]; - + var indexesMaxedOut = GrowIndexesIfNeeded(ref databasesMapSnapshot[dbId]); if (allIndexesMaxedOut && !indexesMaxedOut) allIndexesMaxedOut = false; @@ -971,4 +971,4 @@ public override void Dispose() cts.Dispose(); } } -} +} \ No newline at end of file diff --git a/libs/server/Databases/SingleDatabaseManager.cs b/libs/server/Databases/SingleDatabaseManager.cs index 8d567536e1..5ba377c5e1 100644 --- a/libs/server/Databases/SingleDatabaseManager.cs +++ b/libs/server/Databases/SingleDatabaseManager.cs @@ -19,7 +19,7 @@ internal class SingleDatabaseManager : DatabaseManagerBase GarnetDatabase defaultDatabase; - public SingleDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createDatabaseDelegate, StoreWrapper storeWrapper, bool createDefaultDatabase = true) : + public SingleDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createDatabaseDelegate, StoreWrapper storeWrapper, bool createDefaultDatabase = true) : base(createDatabaseDelegate, storeWrapper) { Logger = storeWrapper.loggerFactory?.CreateLogger(nameof(SingleDatabaseManager)); @@ -86,7 +86,7 @@ public override void RecoverCheckpoint(bool replicaRecover = false, bool recover Logger?.LogInformation(ex, "Error during recovery of store; storeVersion = {storeVersion}; objectStoreVersion = {objectStoreVersion}", storeVersion, objectStoreVersion); - + if (StoreWrapper.serverOptions.FailOnRecoveryError) throw; } @@ -169,7 +169,7 @@ public override async Task TakeOnDemandCheckpointAsync(DateTimeOffset entryTime, // If an external task has taken a checkpoint beyond the provided entryTime return if (!checkpointsPaused || DefaultDatabase.LastSaveTime > entryTime) return; - + // Necessary to take a checkpoint because the latest checkpoint is before entryTime await TakeCheckpointAsync(DefaultDatabase, StoreType.All, logger: Logger); @@ -247,7 +247,7 @@ public override long ReplayAOF(long untilAddress = -1) // When replaying AOF we do not want to write record again to AOF. // So initialize local AofProcessor with recordToAof: false. var aofProcessor = new AofProcessor(StoreWrapper, recordToAof: false, Logger); - + try { return ReplayDatabaseAOF(aofProcessor, ref DefaultDatabase, untilAddress); @@ -344,4 +344,4 @@ public override void Dispose() Disposed = true; } } -} +} \ No newline at end of file diff --git a/libs/server/Metrics/Info/GarnetInfoMetrics.cs b/libs/server/Metrics/Info/GarnetInfoMetrics.cs index 9d8006873e..6fdba2146c 100644 --- a/libs/server/Metrics/Info/GarnetInfoMetrics.cs +++ b/libs/server/Metrics/Info/GarnetInfoMetrics.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Globalization; using System.Linq; +using System.Text; using Garnet.common; namespace Garnet.server @@ -63,11 +64,10 @@ private void PopulateServerInfo(StoreWrapper storeWrapper) private void PopulateMemoryInfo(StoreWrapper storeWrapper) { - var dbCount = storeWrapper.DatabaseCount; - var main_store_index_size = storeWrapper.store.IndexSize * 64 * dbCount; - var main_store_log_memory_size = storeWrapper.store.Log.MemorySizeBytes * dbCount; - var main_store_read_cache_size = storeWrapper.store.ReadCache?.MemorySizeBytes * dbCount ?? 0; - var total_main_store_size = main_store_index_size + main_store_log_memory_size + main_store_read_cache_size; + var main_store_index_size = -1L; + var main_store_log_memory_size = -1L; + var main_store_read_cache_size = -1L; + var total_main_store_size = -1L; var object_store_index_size = -1L; var object_store_log_memory_size = -1L; @@ -75,20 +75,32 @@ private void PopulateMemoryInfo(StoreWrapper storeWrapper) var object_store_heap_memory_size = -1L; var object_store_read_cache_heap_memory_size = -1L; var total_object_store_size = -1L; - var disableObj = storeWrapper.serverOptions.DisableObjects; - var aof_log_memory_size = storeWrapper.appendOnlyFile?.MemorySizeBytes * dbCount ?? - 1; + var aof_log_memory_size = -1L; + + var databases = storeWrapper.databaseManager.GetDatabasesSnapshot(); + var disableObj = storeWrapper.serverOptions.DisableObjects; - if (!disableObj) + foreach (var db in databases) { - object_store_index_size = storeWrapper.objectStore.IndexSize * 64 * dbCount; - object_store_log_memory_size = storeWrapper.objectStore.Log.MemorySizeBytes * dbCount; - object_store_read_cache_log_memory_size = storeWrapper.objectStore.ReadCache?.MemorySizeBytes * dbCount ?? 0; - object_store_heap_memory_size = storeWrapper.objectStoreSizeTracker?.mainLogTracker.LogHeapSizeBytes * dbCount ?? 0; - object_store_read_cache_heap_memory_size = storeWrapper.objectStoreSizeTracker?.readCacheTracker?.LogHeapSizeBytes * dbCount ?? 0; - total_object_store_size = object_store_index_size + object_store_log_memory_size + - object_store_read_cache_log_memory_size + object_store_heap_memory_size + - object_store_read_cache_heap_memory_size; + main_store_index_size += db.MainStore.IndexSize * 64; + main_store_log_memory_size += db.MainStore.Log.MemorySizeBytes; + main_store_read_cache_size += db.MainStore.ReadCache?.MemorySizeBytes ?? 0; + total_main_store_size += main_store_index_size + main_store_log_memory_size + main_store_read_cache_size; + + aof_log_memory_size = db.AppendOnlyFile?.MemorySizeBytes ?? -1; + + if (!disableObj) + { + object_store_index_size += db.ObjectStore.IndexSize * 64; + object_store_log_memory_size += db.ObjectStore.Log.MemorySizeBytes; + object_store_read_cache_log_memory_size += db.ObjectStore.ReadCache?.MemorySizeBytes ?? 0; + object_store_heap_memory_size += db.ObjectStoreSizeTracker?.mainLogTracker.LogHeapSizeBytes ?? 0; + object_store_read_cache_heap_memory_size += db.ObjectStoreSizeTracker?.readCacheTracker?.LogHeapSizeBytes ?? 0; + total_object_store_size += object_store_index_size + object_store_log_memory_size + + object_store_read_cache_log_memory_size + object_store_heap_memory_size + + object_store_read_cache_heap_memory_size; + } } var gcMemoryInfo = GC.GetGCMemoryInfo(); @@ -280,26 +292,88 @@ private MetricsItem[] GetDatabaseObjectStoreStats(StoreWrapper storeWrapper, ref new($"db{db.Id}.ReadCache.TailAddress", db.ObjectStore.ReadCache?.TailAddress.ToString() ?? "N/A"), ]; - private void PopulateStoreHashDistribution(StoreWrapper storeWrapper) => storeHashDistrInfo = [new("", storeWrapper.store.DumpDistribution())]; + private void PopulateStoreHashDistribution(StoreWrapper storeWrapper) + { + var databases = storeWrapper.databaseManager.GetDatabasesSnapshot(); + + var sb = new StringBuilder(); + foreach (var db in databases) + { + sb.AppendLine($"db{db.Id}:"); + sb.Append(db.MainStore.DumpDistribution()); + } + + storeHashDistrInfo = [new("", sb.ToString())]; + } + + private void PopulateObjectStoreHashDistribution(StoreWrapper storeWrapper) + { + var databases = storeWrapper.databaseManager.GetDatabasesSnapshot(); + + var sb = new StringBuilder(); + foreach (var db in databases) + { + sb.AppendLine($"db{db.Id}:"); + sb.Append(db.ObjectStore.DumpDistribution()); + } + + objectStoreHashDistrInfo = [new("", sb.ToString())]; + } + + private void PopulateStoreRevivInfo(StoreWrapper storeWrapper) + { + var databases = storeWrapper.databaseManager.GetDatabasesSnapshot(); + + var sb = new StringBuilder(); + foreach (var db in databases) + { + sb.AppendLine($"db{db.Id}:"); + sb.Append(db.MainStore.DumpRevivificationStats()); + } - private void PopulateObjectStoreHashDistribution(StoreWrapper storeWrapper) => objectStoreHashDistrInfo = [new("", storeWrapper.objectStore.DumpDistribution())]; + storeRevivInfo = [new("", sb.ToString())]; + } - private void PopulateStoreRevivInfo(StoreWrapper storeWrapper) => storeRevivInfo = [new("", storeWrapper.store.DumpRevivificationStats())]; + private void PopulateObjectStoreRevivInfo(StoreWrapper storeWrapper) + { + var databases = storeWrapper.databaseManager.GetDatabasesSnapshot(); + var sb = new StringBuilder(); + foreach (var db in databases) + { + sb.AppendLine($"db{db.Id}:"); + sb.Append(db.ObjectStore.DumpRevivificationStats()); + } - private void PopulateObjectStoreRevivInfo(StoreWrapper storeWrapper) => objectStoreRevivInfo = [new("", storeWrapper.objectStore.DumpRevivificationStats())]; + objectStoreRevivInfo = [new("", sb.ToString())]; + } private void PopulatePersistenceInfo(StoreWrapper storeWrapper) { - bool aofEnabled = storeWrapper.serverOptions.EnableAOF; - persistenceInfo = - [ - new("CommittedBeginAddress", !aofEnabled ? "N/A" : storeWrapper.appendOnlyFile.CommittedBeginAddress.ToString()), - new("CommittedUntilAddress", !aofEnabled ? "N/A" : storeWrapper.appendOnlyFile.CommittedUntilAddress.ToString()), - new("FlushedUntilAddress", !aofEnabled ? "N/A" : storeWrapper.appendOnlyFile.FlushedUntilAddress.ToString()), - new("BeginAddress", !aofEnabled ? "N/A" : storeWrapper.appendOnlyFile.BeginAddress.ToString()), - new("TailAddress", !aofEnabled ? "N/A" : storeWrapper.appendOnlyFile.TailAddress.ToString()), - new("SafeAofAddress", !aofEnabled ? "N/A" : storeWrapper.safeAofAddress.ToString()) - ]; + var databases = storeWrapper.databaseManager.GetDatabasesSnapshot(); + + for (var i = 0; i < databases.Length; i++) + { + var persistenceStats = GetDatabasePersistenceStats(storeWrapper, ref databases[i]); + if (i == 0) + persistenceInfo = new MetricsItem[databases.Length * persistenceStats.Length]; + + Array.Copy(persistenceStats, 0, persistenceInfo, persistenceStats.Length * i, persistenceStats.Length); + } + } + + private MetricsItem[] GetDatabasePersistenceStats(StoreWrapper storeWrapper, ref GarnetDatabase db) + { + var aofEnabled = storeWrapper.serverOptions.EnableAOF; + + return + [ + new($"db{db.Id}.CommittedBeginAddress", !aofEnabled ? "N/A" : db.AppendOnlyFile.CommittedBeginAddress.ToString()), + new($"db{db.Id}.CommittedUntilAddress", !aofEnabled ? "N/A" : db.AppendOnlyFile.CommittedUntilAddress.ToString()), + new($"db{db.Id}.FlushedUntilAddress", !aofEnabled ? "N/A" : db.AppendOnlyFile.FlushedUntilAddress.ToString()), + new($"db{db.Id}.BeginAddress", !aofEnabled ? "N/A" : db.AppendOnlyFile.BeginAddress.ToString()), + new($"db{db.Id}.TailAddress", !aofEnabled ? "N/A" : db.AppendOnlyFile.TailAddress.ToString()), + new($"db{db.Id}.SafeAofAddress", !aofEnabled ? "N/A" : storeWrapper.safeAofAddress.ToString()) + ]; } private void PopulateClientsInfo(StoreWrapper storeWrapper) @@ -388,25 +462,25 @@ public string GetRespInfo(InfoMetricsType section, StoreWrapper storeWrapper) PopulateStoreStats(storeWrapper); return GetSectionRespInfo(InfoMetricsType.STORE, storeInfo); case InfoMetricsType.OBJECTSTORE: - if (storeWrapper.objectStore == null) return ""; + if (storeWrapper.serverOptions.DisableObjects) return ""; PopulateObjectStoreStats(storeWrapper); return GetSectionRespInfo(InfoMetricsType.OBJECTSTORE, objectStoreInfo); case InfoMetricsType.STOREHASHTABLE: PopulateStoreHashDistribution(storeWrapper); return GetSectionRespInfo(InfoMetricsType.STOREHASHTABLE, storeHashDistrInfo); case InfoMetricsType.OBJECTSTOREHASHTABLE: - if (storeWrapper.objectStore == null) return ""; + if (storeWrapper.serverOptions.DisableObjects) return ""; PopulateObjectStoreHashDistribution(storeWrapper); return GetSectionRespInfo(InfoMetricsType.OBJECTSTOREHASHTABLE, objectStoreHashDistrInfo); case InfoMetricsType.STOREREVIV: PopulateStoreRevivInfo(storeWrapper); return GetSectionRespInfo(InfoMetricsType.STOREREVIV, storeRevivInfo); case InfoMetricsType.OBJECTSTOREREVIV: - if (storeWrapper.objectStore == null) return ""; + if (storeWrapper.serverOptions.DisableObjects) return ""; PopulateObjectStoreRevivInfo(storeWrapper); return GetSectionRespInfo(InfoMetricsType.OBJECTSTOREREVIV, objectStoreRevivInfo); case InfoMetricsType.PERSISTENCE: - if (storeWrapper.appendOnlyFile == null) return ""; + if (!storeWrapper.serverOptions.EnableAOF) return ""; PopulatePersistenceInfo(storeWrapper); return GetSectionRespInfo(InfoMetricsType.PERSISTENCE, persistenceInfo); case InfoMetricsType.CLIENTS: @@ -462,25 +536,25 @@ private MetricsItem[] GetMetricInternal(InfoMetricsType section, StoreWrapper st PopulateStoreStats(storeWrapper); return storeInfo; case InfoMetricsType.OBJECTSTORE: - if (storeWrapper.objectStore == null) return null; + if (storeWrapper.serverOptions.DisableObjects) return null; PopulateObjectStoreStats(storeWrapper); return objectStoreInfo; case InfoMetricsType.STOREHASHTABLE: PopulateStoreHashDistribution(storeWrapper); return storeHashDistrInfo; case InfoMetricsType.OBJECTSTOREHASHTABLE: - if (storeWrapper.objectStore == null) return null; + if (storeWrapper.serverOptions.DisableObjects) return null; PopulateObjectStoreHashDistribution(storeWrapper); return objectStoreHashDistrInfo; case InfoMetricsType.STOREREVIV: PopulateStoreRevivInfo(storeWrapper); return storeRevivInfo; case InfoMetricsType.OBJECTSTOREREVIV: - if (storeWrapper.objectStore == null) return null; + if (storeWrapper.serverOptions.DisableObjects) return null; PopulateObjectStoreRevivInfo(storeWrapper); return objectStoreRevivInfo; case InfoMetricsType.PERSISTENCE: - if (storeWrapper.appendOnlyFile == null) return null; + if (!storeWrapper.serverOptions.EnableAOF) return null; PopulatePersistenceInfo(storeWrapper); return persistenceInfo; case InfoMetricsType.CLIENTS: diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 6cfe6e74ac..19ee7f192e 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -227,14 +227,14 @@ public StoreWrapper( /// Source instance /// Enable AOF in database manager public StoreWrapper(StoreWrapper storeWrapper, bool recordToAof) : this(storeWrapper.version, - storeWrapper.redisProtocolVersion, + storeWrapper.redisProtocolVersion, storeWrapper.server, - storeWrapper.customCommandManager, + storeWrapper.customCommandManager, storeWrapper.serverOptions, - storeWrapper.subscribeBroker, + storeWrapper.subscribeBroker, storeWrapper.accessControlList, - databaseManager: storeWrapper.databaseManager.Clone(recordToAof), - clusterFactory: null, + databaseManager: storeWrapper.databaseManager.Clone(recordToAof), + clusterFactory: null, loggerFactory: storeWrapper.loggerFactory) { } From 8476e35389b00d9a59cd364e6844f779a191fc17 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Tue, 4 Mar 2025 18:41:02 -0800 Subject: [PATCH 69/82] format --- libs/server/StoreWrapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index a8ed366df0..5249abc8b7 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -104,7 +104,7 @@ public sealed class StoreWrapper /// Lua script cache /// public readonly ConcurrentDictionary storeScriptCache; - + /// /// Logging frequency /// From 52e8339111ed7f40617afeb3407991c3b3b56e3d Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Wed, 5 Mar 2025 11:32:52 -0800 Subject: [PATCH 70/82] some merge fixes --- .../DisklessReplication/ReplicationSnapshotIterator.cs | 3 --- libs/cluster/Server/Replication/ReplicationManager.cs | 2 +- libs/server/Databases/DatabaseManagerBase.cs | 2 +- libs/server/Databases/IDatabaseManager.cs | 4 ++-- libs/server/Databases/MultiDatabaseManager.cs | 4 ++-- libs/server/Databases/SingleDatabaseManager.cs | 4 ++-- libs/server/StoreWrapper.cs | 6 +++--- test/Garnet.test.cluster/ClusterTestUtils.cs | 8 +++++++- 8 files changed, 18 insertions(+), 15 deletions(-) diff --git a/libs/cluster/Server/Replication/PrimaryOps/DisklessReplication/ReplicationSnapshotIterator.cs b/libs/cluster/Server/Replication/PrimaryOps/DisklessReplication/ReplicationSnapshotIterator.cs index 36cdcdf9c9..30a04798d3 100644 --- a/libs/cluster/Server/Replication/PrimaryOps/DisklessReplication/ReplicationSnapshotIterator.cs +++ b/libs/cluster/Server/Replication/PrimaryOps/DisklessReplication/ReplicationSnapshotIterator.cs @@ -195,9 +195,6 @@ public void OnStop(bool completed, long numberOfRecords, bool isMainStore, long // Wait for flush and response to complete replicationSyncManager.WaitForFlush().GetAwaiter().GetResult(); - // Enqueue version change commit - replicationSyncManager.ClusterProvider.storeWrapper.EnqueueCommit(isMainStore, targetVersion); - logger?.LogTrace("{OnStop} {store} {numberOfRecords} {targetVersion}", nameof(OnStop), isMainStore ? "MAIN STORE" : "OBJECT STORE", numberOfRecords, targetVersion); diff --git a/libs/cluster/Server/Replication/ReplicationManager.cs b/libs/cluster/Server/Replication/ReplicationManager.cs index 1920f2dbee..072bffba82 100644 --- a/libs/cluster/Server/Replication/ReplicationManager.cs +++ b/libs/cluster/Server/Replication/ReplicationManager.cs @@ -156,7 +156,7 @@ void CheckpointVersionShift(bool isMainStore, long oldVersion, long newVersion) { if (clusterProvider.clusterManager.CurrentConfig.LocalNodeRole == NodeRole.REPLICA) return; - storeWrapper.EnqueueCommit(isMainStore, newVersion, streaming: true); + storeWrapper.EnqueueCommit(isMainStore, newVersion, diskless: clusterProvider.serverOptions.ReplicaDisklessSync); } /// diff --git a/libs/server/Databases/DatabaseManagerBase.cs b/libs/server/Databases/DatabaseManagerBase.cs index cd76d602b2..55ea871735 100644 --- a/libs/server/Databases/DatabaseManagerBase.cs +++ b/libs/server/Databases/DatabaseManagerBase.cs @@ -113,7 +113,7 @@ public abstract Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLimit, public abstract void ResetRevivificationStats(); /// - public abstract void EnqueueCommit(bool isMainStore, long version, int dbId = 0, bool streaming = false); + public abstract void EnqueueCommit(bool isMainStore, long version, int dbId = 0, bool diskless = false); /// public abstract GarnetDatabase[] GetDatabasesSnapshot(); diff --git a/libs/server/Databases/IDatabaseManager.cs b/libs/server/Databases/IDatabaseManager.cs index fcfe0b5e8d..5d0516bde5 100644 --- a/libs/server/Databases/IDatabaseManager.cs +++ b/libs/server/Databases/IDatabaseManager.cs @@ -188,8 +188,8 @@ public Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLimit, Cancellati /// /// /// - /// - public void EnqueueCommit(bool isMainStore, long version, int dbId = 0, bool streaming = false); + /// + public void EnqueueCommit(bool isMainStore, long version, int dbId = 0, bool diskless = false); /// /// Get a snapshot of all active databases diff --git a/libs/server/Databases/MultiDatabaseManager.cs b/libs/server/Databases/MultiDatabaseManager.cs index 16f18e1c02..11e90774dd 100644 --- a/libs/server/Databases/MultiDatabaseManager.cs +++ b/libs/server/Databases/MultiDatabaseManager.cs @@ -540,12 +540,12 @@ public override void ResetRevivificationStats() } } - public override void EnqueueCommit(bool isMainStore, long version, int dbId = 0, bool streaming = false) + public override void EnqueueCommit(bool isMainStore, long version, int dbId = 0, bool diskless = false) { if (!TryGetOrAddDatabase(dbId, out var db)) throw new GarnetException($"Database with ID {dbId} was not found."); - EnqueueDatabaseCommit(ref db, isMainStore, version, streaming); + EnqueueDatabaseCommit(ref db, isMainStore, version, diskless); } /// diff --git a/libs/server/Databases/SingleDatabaseManager.cs b/libs/server/Databases/SingleDatabaseManager.cs index 51c4cf7f31..d2a320718f 100644 --- a/libs/server/Databases/SingleDatabaseManager.cs +++ b/libs/server/Databases/SingleDatabaseManager.cs @@ -284,11 +284,11 @@ public override void ResetRevivificationStats() } /// - public override void EnqueueCommit(bool isMainStore, long version, int dbId = 0, bool streaming = false) + public override void EnqueueCommit(bool isMainStore, long version, int dbId = 0, bool diskless = false) { ArgumentOutOfRangeException.ThrowIfNotEqual(dbId, 0); - EnqueueDatabaseCommit(ref DefaultDatabase, isMainStore, version, streaming); + EnqueueDatabaseCommit(ref DefaultDatabase, isMainStore, version, diskless); } public override GarnetDatabase[] GetDatabasesSnapshot() => [DefaultDatabase]; diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 5249abc8b7..5ee268eea9 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -325,9 +325,9 @@ public void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStore /// /// /// - /// - public void EnqueueCommit(bool isMainStore, long version, int dbId = 0, bool streaming = false) => - this.databaseManager.EnqueueCommit(isMainStore, version, dbId, streaming); + /// + public void EnqueueCommit(bool isMainStore, long version, int dbId = 0, bool diskless = false) => + this.databaseManager.EnqueueCommit(isMainStore, version, dbId, diskless); /// /// Reset diff --git a/test/Garnet.test.cluster/ClusterTestUtils.cs b/test/Garnet.test.cluster/ClusterTestUtils.cs index 668bf78b58..e7c772d55d 100644 --- a/test/Garnet.test.cluster/ClusterTestUtils.cs +++ b/test/Garnet.test.cluster/ClusterTestUtils.cs @@ -2761,8 +2761,14 @@ public int GetStoreCurrentVersion(int nodeIndex, bool isMainStore, ILogger logge if (line.StartsWith('#')) continue; var field = line.Trim().Split(':'); + + // Remove 'db0.' prefix + var sepIdx = field[0].IndexOf('.'); + if (sepIdx == -1) + continue; + var key = field[0].Substring(sepIdx + 1); - if (!Enum.TryParse(field[0], ignoreCase: true, out StoreInfoItem type)) + if (!Enum.TryParse(key, ignoreCase: true, out StoreInfoItem type)) continue; if (infoItems.Contains(type)) From d6567a11d4a36f54c9556c4546f2e63cb8771fc4 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Wed, 5 Mar 2025 11:39:07 -0800 Subject: [PATCH 71/82] format --- test/Garnet.test.cluster/ClusterTestUtils.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Garnet.test.cluster/ClusterTestUtils.cs b/test/Garnet.test.cluster/ClusterTestUtils.cs index e7c772d55d..354d034b20 100644 --- a/test/Garnet.test.cluster/ClusterTestUtils.cs +++ b/test/Garnet.test.cluster/ClusterTestUtils.cs @@ -2761,10 +2761,10 @@ public int GetStoreCurrentVersion(int nodeIndex, bool isMainStore, ILogger logge if (line.StartsWith('#')) continue; var field = line.Trim().Split(':'); - + // Remove 'db0.' prefix var sepIdx = field[0].IndexOf('.'); - if (sepIdx == -1) + if (sepIdx == -1) continue; var key = field[0].Substring(sepIdx + 1); From 8b3e300a272bdff98513a64d194a3e7c8d831a5a Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Wed, 5 Mar 2025 12:11:54 -0800 Subject: [PATCH 72/82] bugfix --- libs/host/GarnetServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/host/GarnetServer.cs b/libs/host/GarnetServer.cs index 46bd13ae77..571bf4a66a 100644 --- a/libs/host/GarnetServer.cs +++ b/libs/host/GarnetServer.cs @@ -315,7 +315,7 @@ private TsavoriteKV { kvSettings = opts.GetSettings(loggerFactory, out logFactory); - checkpointDir = opts.CheckpointDir ?? opts.LogDir; + checkpointDir = (opts.CheckpointDir ?? opts.LogDir) ?? string.Empty; // Run checkpoint on its own thread to control p99 kvSettings.ThrottleCheckpointFlushDelayMs = opts.CheckpointThrottleFlushDelayMs; From 53dc7c1deda65096a8c84b45c93f0028f6459ef2 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Wed, 5 Mar 2025 15:34:10 -0800 Subject: [PATCH 73/82] fix --- libs/server/Servers/GarnetServerOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/server/Servers/GarnetServerOptions.cs b/libs/server/Servers/GarnetServerOptions.cs index d96af6a066..71c0bef124 100644 --- a/libs/server/Servers/GarnetServerOptions.cs +++ b/libs/server/Servers/GarnetServerOptions.cs @@ -758,7 +758,7 @@ public void GetAofSettings(int dbId, out TsavoriteLogSettings tsavoriteLogSettin throw new Exception("AOF Page size cannot be more than the AOF memory size."); } - aofDir = Path.Combine(CheckpointDir, $"AOF{(dbId == 0 ? string.Empty : $"_{dbId}")}"); + aofDir = Path.Combine(CheckpointDir ?? string.Empty, $"AOF{(dbId == 0 ? string.Empty : $"_{dbId}")}"); tsavoriteLogSettings.LogCommitManager = new DeviceLogCommitCheckpointManager( FastAofTruncate ? new NullNamedDeviceFactoryCreator() : DeviceFactoryCreator, new DefaultCheckpointNamingScheme(aofDir), From 9583a97b8befc3c9a662d8dd7208140a71c50bb6 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 6 Mar 2025 19:54:07 -0800 Subject: [PATCH 74/82] Added ServerOperations to BDN + allocation improvements --- .github/workflows/ci-bdnbenchmark.yml | 2 +- .../Operations/ServerOperations.cs | 52 +++++++++ libs/server/Custom/ExpandableMap.cs | 43 +------- libs/server/Custom/IDefaultChecker.cs | 10 ++ libs/server/Databases/DatabaseManagerBase.cs | 4 +- libs/server/Databases/IDatabaseManager.cs | 13 +-- libs/server/Databases/MultiDatabaseManager.cs | 102 +++++++++++++----- .../server/Databases/SingleDatabaseManager.cs | 13 +-- libs/server/GarnetDatabase.cs | 7 +- libs/server/Resp/AdminCommands.cs | 4 +- libs/server/Resp/ArrayCommands.cs | 2 +- libs/server/Resp/CmdStrings.cs | 1 + libs/server/Resp/GarnetDatabaseSession.cs | 17 ++- libs/server/Resp/RespServerSession.cs | 62 +++++++++-- libs/server/Storage/Session/StorageSession.cs | 2 +- libs/server/StoreWrapper.cs | 2 +- test/BDNPerfTests/BDN_Benchmark_Config.json | 8 ++ test/Garnet.test/MultiDatabaseTests.cs | 63 +++++++++-- 18 files changed, 299 insertions(+), 108 deletions(-) create mode 100644 benchmark/BDN.benchmark/Operations/ServerOperations.cs create mode 100644 libs/server/Custom/IDefaultChecker.cs diff --git a/.github/workflows/ci-bdnbenchmark.yml b/.github/workflows/ci-bdnbenchmark.yml index a0b6921562..0b9dc3c68a 100644 --- a/.github/workflows/ci-bdnbenchmark.yml +++ b/.github/workflows/ci-bdnbenchmark.yml @@ -42,7 +42,7 @@ jobs: os: [ ubuntu-latest, windows-latest ] framework: [ 'net8.0' ] configuration: [ 'Release' ] - test: [ 'Operations.BasicOperations', 'Operations.ObjectOperations', 'Operations.HashObjectOperations', 'Operations.SortedSetOperations', 'Cluster.ClusterMigrate', 'Cluster.ClusterOperations', 'Lua.LuaScripts', 'Lua.LuaScriptCacheOperations','Lua.LuaRunnerOperations','Operations.CustomOperations', 'Operations.RawStringOperations', 'Operations.ScriptOperations', 'Operations.ModuleOperations', 'Operations.PubSubOperations', 'Network.BasicOperations', 'Network.RawStringOperations' ] + test: [ 'Operations.BasicOperations', 'Operations.ObjectOperations', 'Operations.HashObjectOperations', 'Operations.SortedSetOperations', 'Cluster.ClusterMigrate', 'Cluster.ClusterOperations', 'Lua.LuaScripts', 'Lua.LuaScriptCacheOperations','Lua.LuaRunnerOperations','Operations.CustomOperations', 'Operations.RawStringOperations', 'Operations.ScriptOperations', 'Operations.ModuleOperations', 'Operations.PubSubOperations', 'Operations.ServerOperations', 'Network.BasicOperations', 'Network.RawStringOperations' ] steps: - name: Check out code uses: actions/checkout@v4 diff --git a/benchmark/BDN.benchmark/Operations/ServerOperations.cs b/benchmark/BDN.benchmark/Operations/ServerOperations.cs new file mode 100644 index 0000000000..bd78fd0575 --- /dev/null +++ b/benchmark/BDN.benchmark/Operations/ServerOperations.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using BenchmarkDotNet.Attributes; +using Embedded.server; + +namespace BDN.benchmark.Operations +{ + /// + /// Benchmark for ServerOperations + /// + [MemoryDiagnoser] + public unsafe class ServerOperations : OperationsBase + { + static ReadOnlySpan SELECTUNSELECT => "*2\r\n$6\r\nSELECT\r\n$1\r\n1\r\n*2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n"u8; + Request selectUnselect; + + static ReadOnlySpan SWAPDB => "*3\r\n$6\r\nSWAPDB\r\n$1\r\n1\r\n$1\r\n0\r\n"u8; + Request swapDb; + + public override void GlobalSetup() + { + base.GlobalSetup(); + + SetupOperation(ref selectUnselect, SELECTUNSELECT); + SetupOperation(ref swapDb, SWAPDB); + + // Pre-populate data in DB0 + SlowConsumeMessage("*3\r\n$3\r\nSET\r\n$1\r\na\r\n$1\r\na\r\n"u8); + SlowConsumeMessage("*3\r\n$5\r\nLPUSH\r\n$1\r\nd\r\n$1\r\nf\r\n"u8); + SlowConsumeMessage("*3\r\n$4\r\nSADD\r\n$1\r\ne\r\n$1\r\nb\r\n"u8); + + // Pre-populate data in DB1 + SlowConsumeMessage("*2\r\n$6\r\nSELECT\r\n$1\r\n1\r\n"u8); + SlowConsumeMessage("*3\r\n$3\r\nSET\r\n$1\r\nb\r\n$1\r\nb\r\n"u8); + SlowConsumeMessage("*3\r\n$5\r\nLPUSH\r\n$1\r\nf\r\n$1\r\nh\r\n"u8); + SlowConsumeMessage("*3\r\n$4\r\nSADD\r\n$1\r\ng\r\n$1\r\ni\r\n"u8); + } + + [Benchmark] + public void SelectUnselect() + { + Send(selectUnselect); + } + + [Benchmark] + public void SwapDb() + { + Send(swapDb); + } + } +} \ No newline at end of file diff --git a/libs/server/Custom/ExpandableMap.cs b/libs/server/Custom/ExpandableMap.cs index 0303142460..f77cf75e5f 100644 --- a/libs/server/Custom/ExpandableMap.cs +++ b/libs/server/Custom/ExpandableMap.cs @@ -76,47 +76,6 @@ public bool TryGetValue(int id, out T value) return true; } - /// - /// If value exists for specified ID, return it, - /// otherwise set it using the provided value factory - /// - /// Item ID - /// Value factory - /// Returned value - /// True if item was not previously set, but was set by this method - /// True if item was previously initialized or set successfully - public bool TryGetOrSet(int id, Func valueFactory, out T value, out bool added) - { - added = false; - - // Try to get the current value, if value is already set, return it - if (this.TryGetValue(id, out var currValue) && !currValue.Equals(default(T))) - { - value = currValue; - return true; - } - - mapLock.WriteLock(); - try - { - // Try to get the current value, if value is already set, return it - if (this.TryGetValue(id, out currValue) && !currValue.Equals(default(T))) - { - value = currValue; - return true; - } - - // Try to set value with expanding the map, if needed - value = valueFactory(); - added = this.TrySetValueUnsafe(id, ref value, noExpansion: false); - return added; - } - finally - { - mapLock.WriteUnlock(); - } - } - /// /// Try to set item by ID /// @@ -236,7 +195,7 @@ private bool TryUpdateActualSize(int id) /// Item value /// True if should not attempt to expand the underlying array /// True if assignment succeeded - private bool TrySetValueUnsafe(int id, ref T value, bool noExpansion) + internal bool TrySetValueUnsafe(int id, ref T value, bool noExpansion) { var idx = id - minId; if (idx < 0 || idx >= maxSize) return false; diff --git a/libs/server/Custom/IDefaultChecker.cs b/libs/server/Custom/IDefaultChecker.cs new file mode 100644 index 0000000000..cd049ef9ac --- /dev/null +++ b/libs/server/Custom/IDefaultChecker.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Garnet.server +{ + public interface IDefaultChecker + { + public bool IsDefault(); + } +} \ No newline at end of file diff --git a/libs/server/Databases/DatabaseManagerBase.cs b/libs/server/Databases/DatabaseManagerBase.cs index 55ea871735..d76db78ae4 100644 --- a/libs/server/Databases/DatabaseManagerBase.cs +++ b/libs/server/Databases/DatabaseManagerBase.cs @@ -47,7 +47,7 @@ internal abstract class DatabaseManagerBase : IDatabaseManager public readonly StoreWrapper StoreWrapper; /// - public abstract bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db); + public abstract ref GarnetDatabase TryGetOrAddDatabase(int dbId, out bool success, out bool added); /// public abstract bool TryPauseCheckpoints(int dbId); @@ -119,7 +119,7 @@ public abstract Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLimit, public abstract GarnetDatabase[] GetDatabasesSnapshot(); /// - public abstract bool TryGetDatabase(int dbId, out GarnetDatabase db); + public abstract ref GarnetDatabase TryGetDatabase(int dbId, out bool found); /// public abstract void FlushDatabase(bool unsafeTruncateLog, int dbId = 0); diff --git a/libs/server/Databases/IDatabaseManager.cs b/libs/server/Databases/IDatabaseManager.cs index 5d0516bde5..a308092287 100644 --- a/libs/server/Databases/IDatabaseManager.cs +++ b/libs/server/Databases/IDatabaseManager.cs @@ -61,9 +61,10 @@ public interface IDatabaseManager : IDisposable /// Try to get or add a new database /// /// Database ID - /// Database - /// True if database was retrieved or added successfully - public bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db); + /// Database was found or added successfully + /// True if database was added + /// Reference to retrieved or added database + public ref GarnetDatabase TryGetOrAddDatabase(int dbId, out bool success, out bool added); /// /// Mark the beginning of a checkpoint by taking and a lock to avoid concurrent checkpointing @@ -201,9 +202,9 @@ public Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLimit, Cancellati /// Get database DB ID /// /// DB Id - /// Database - /// True if database found - public bool TryGetDatabase(int dbId, out GarnetDatabase db); + /// True if database was found + /// Reference to database + public ref GarnetDatabase TryGetDatabase(int dbId, out bool found); /// /// Flush database with specified ID diff --git a/libs/server/Databases/MultiDatabaseManager.cs b/libs/server/Databases/MultiDatabaseManager.cs index 11e90774dd..056ab5aeba 100644 --- a/libs/server/Databases/MultiDatabaseManager.cs +++ b/libs/server/Databases/MultiDatabaseManager.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Garnet.common; @@ -120,7 +121,8 @@ public override void RecoverCheckpoint(bool replicaRecover = false, bool recover foreach (var dbId in dbIdsToRecover) { - if (!TryGetOrAddDatabase(dbId, out var db)) + ref var db = ref TryGetOrAddDatabase(dbId, out var success, out _); + if (!success) throw new GarnetException($"Failed to retrieve or create database for checkpoint recovery (DB ID = {dbId})."); try @@ -377,7 +379,8 @@ public override void RecoverAOF() foreach (var dbId in dbIdsToRecover) { - if (!TryGetOrAddDatabase(dbId, out var db)) + ref var db = ref TryGetOrAddDatabase(dbId, out var success, out _); + if (!success) throw new GarnetException($"Failed to retrieve or create database for AOF recovery (DB ID = {dbId})."); RecoverDatabaseAOF(ref db); @@ -519,7 +522,8 @@ public override void StartObjectSizeTrackers(CancellationToken token = default) /// public override void Reset(int dbId = 0) { - if (!TryGetOrAddDatabase(dbId, out var db)) + ref var db = ref TryGetOrAddDatabase(dbId, out var success, out _); + if (!success) throw new GarnetException($"Database with ID {dbId} was not found."); ResetDatabase(ref db); @@ -542,7 +546,8 @@ public override void ResetRevivificationStats() public override void EnqueueCommit(bool isMainStore, long version, int dbId = 0, bool diskless = false) { - if (!TryGetOrAddDatabase(dbId, out var db)) + ref var db = ref TryGetOrAddDatabase(dbId, out var success, out _); + if (!success) throw new GarnetException($"Database with ID {dbId} was not found."); EnqueueDatabaseCommit(ref db, isMainStore, version, diskless); @@ -570,18 +575,26 @@ public override bool TrySwapDatabases(int dbId1, int dbId2) { if (dbId1 == dbId2) return true; - if (!TryGetOrAddDatabase(dbId1, out var db1) || - !TryGetOrAddDatabase(dbId2, out var db2)) + ref var db1 = ref TryGetOrAddDatabase(dbId1, out var success, out _); + if (!success) + return false; + + ref var db2 = ref TryGetOrAddDatabase(dbId2, out success, out _); + if (!success) return false; databasesLock.WriteLock(); try { var databaseMapSnapshot = databases.Map; - databaseMapSnapshot[dbId2] = db1; + var tmp = db1; databaseMapSnapshot[dbId1] = db2; + databaseMapSnapshot[dbId2] = tmp; + + var sessions = StoreWrapper.TcpServer?.ActiveConsumers().ToArray(); + if (sessions == null) return true; + if (sessions.Length > 1) return false; - var sessions = StoreWrapper.TcpServer.ActiveConsumers(); foreach (var session in sessions) { if (session is not RespServerSession respServerSession) continue; @@ -611,7 +624,8 @@ protected override ref GarnetDatabase GetDatabaseByRef(int dbId = 0) public override FunctionsState CreateFunctionsState(int dbId = 0) { - if (!TryGetOrAddDatabase(dbId, out var db)) + ref var db = ref TryGetOrAddDatabase(dbId, out var success, out _); + if (!success) throw new GarnetException($"Database with ID {dbId} was not found."); return new(db.AppendOnlyFile, db.VersionMap, StoreWrapper.customCommandManager, null, db.ObjectStoreSizeTracker, @@ -619,21 +633,53 @@ public override FunctionsState CreateFunctionsState(int dbId = 0) } /// - public override bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db) + public override ref GarnetDatabase TryGetOrAddDatabase(int dbId, out bool success, out bool added) { - if (!databases.TryGetOrSet(dbId, () => CreateDatabaseDelegate(dbId, out _, out _), out db, out var added)) - return false; + added = false; + success = false; - if (added) - HandleDatabaseAdded(dbId); + var databasesMapSize = databases.ActualSize; + var databasesMapSnapshot = databases.Map; - return true; + if (dbId >= 0 && dbId < databasesMapSize && !databasesMapSnapshot[dbId].IsDefault()) + { + success = true; + return ref databasesMapSnapshot[dbId]; + } + + databases.mapLock.WriteLock(); + + try + { + if (dbId >= 0 && dbId < databasesMapSize && !databasesMapSnapshot[dbId].IsDefault()) + { + success = true; + return ref databasesMapSnapshot[dbId]; + } + + var db = CreateDatabaseDelegate(dbId, out _, out _); + if (!databases.TrySetValueUnsafe(dbId, ref db, false)) + return ref GarnetDatabase.Empty; + } + finally + { + databases.mapLock.WriteUnlock(); + } + + added = true; + success = true; + + HandleDatabaseAdded(dbId); + + databasesMapSnapshot = databases.Map; + return ref databasesMapSnapshot[dbId]; } /// public override bool TryPauseCheckpoints(int dbId) { - if (!TryGetOrAddDatabase(dbId, out var db)) + ref var db = ref TryGetOrAddDatabase(dbId, out var success, out _); + if (!success) throw new GarnetException($"Database with ID {dbId} was not found."); return TryPauseCheckpoints(ref db); @@ -668,33 +714,39 @@ public override void ResumeCheckpoints(int dbId) } /// - public override bool TryGetDatabase(int dbId, out GarnetDatabase db) + public override ref GarnetDatabase TryGetDatabase(int dbId, out bool found) { + found = false; + var databasesMapSize = databases.ActualSize; var databasesMapSnapshot = databases.Map; if (dbId == 0) { - db = databasesMapSnapshot[0]; - Debug.Assert(!db.IsDefault()); - return true; + Debug.Assert(!databasesMapSnapshot[0].IsDefault()); + found = true; + return ref databasesMapSnapshot[0]; } // Check if database already exists if (dbId < databasesMapSize) { - db = databasesMapSnapshot[dbId]; - if (!db.IsDefault()) return true; + if (!databasesMapSnapshot[dbId].IsDefault()) + { + found = true; + return ref databasesMapSnapshot[dbId]; + } } - // Try to retrieve or add database - return TryGetOrAddDatabase(dbId, out db); + found = false; + return ref GarnetDatabase.Empty; } /// public override void FlushDatabase(bool unsafeTruncateLog, int dbId = 0) { - if (!TryGetOrAddDatabase(dbId, out var db)) + ref var db = ref TryGetOrAddDatabase(dbId, out var success, out _); + if (!success) throw new GarnetException($"Database with ID {dbId} was not found."); FlushDatabase(ref db, unsafeTruncateLog); diff --git a/libs/server/Databases/SingleDatabaseManager.cs b/libs/server/Databases/SingleDatabaseManager.cs index d2a320718f..402e7f012d 100644 --- a/libs/server/Databases/SingleDatabaseManager.cs +++ b/libs/server/Databases/SingleDatabaseManager.cs @@ -37,12 +37,13 @@ public SingleDatabaseManager(SingleDatabaseManager src, bool enableAof) : this(s } /// - public override bool TryGetOrAddDatabase(int dbId, out GarnetDatabase db) + public override ref GarnetDatabase TryGetOrAddDatabase(int dbId, out bool success, out bool added) { ArgumentOutOfRangeException.ThrowIfNotEqual(dbId, 0); - db = DefaultDatabase; - return true; + success = true; + added = false; + return ref DefaultDatabase; } public override void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStoreFromToken = false, bool recoverObjectStoreFromToken = false, CheckpointMetadata metadata = null) @@ -294,12 +295,12 @@ public override void EnqueueCommit(bool isMainStore, long version, int dbId = 0, public override GarnetDatabase[] GetDatabasesSnapshot() => [DefaultDatabase]; /// - public override bool TryGetDatabase(int dbId, out GarnetDatabase db) + public override ref GarnetDatabase TryGetDatabase(int dbId, out bool found) { ArgumentOutOfRangeException.ThrowIfNotEqual(dbId, 0); - db = DefaultDatabase; - return true; + found = true; + return ref DefaultDatabase; } /// diff --git a/libs/server/GarnetDatabase.cs b/libs/server/GarnetDatabase.cs index 77f31328fd..0c7496194c 100644 --- a/libs/server/GarnetDatabase.cs +++ b/libs/server/GarnetDatabase.cs @@ -14,7 +14,7 @@ namespace Garnet.server /// /// Represents a logical database in Garnet /// - public struct GarnetDatabase : IDisposable + public struct GarnetDatabase : IDefaultChecker, IDisposable { /// /// Default size for version map @@ -146,5 +146,10 @@ public void Dispose() disposed = true; } + + /// + /// Instance of empty database + /// + internal static GarnetDatabase Empty; } } \ No newline at end of file diff --git a/libs/server/Resp/AdminCommands.cs b/libs/server/Resp/AdminCommands.cs index c0a73a4101..2bf1562a51 100644 --- a/libs/server/Resp/AdminCommands.cs +++ b/libs/server/Resp/AdminCommands.cs @@ -934,8 +934,8 @@ private bool NetworkLASTSAVE() } } - storeWrapper.databaseManager.TryGetDatabase(dbId, out var db); - + ref var db = ref storeWrapper.databaseManager.TryGetOrAddDatabase(dbId, out var success, out _); + Debug.Assert(success); var seconds = db.LastSaveTime.ToUnixTimeSeconds(); while (!RespWriteUtils.TryWriteInt64(seconds, ref dcurr, dend)) SendAndReset(); diff --git a/libs/server/Resp/ArrayCommands.cs b/libs/server/Resp/ArrayCommands.cs index 3f839d05ec..8d6c61df51 100644 --- a/libs/server/Resp/ArrayCommands.cs +++ b/libs/server/Resp/ArrayCommands.cs @@ -290,7 +290,7 @@ private bool NetworkSWAPDB() } else { - while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_DB_INDEX_OUT_OF_RANGE, ref dcurr, dend)) + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_DBSWAP_UNSUPPORTED, ref dcurr, dend)) SendAndReset(); } } diff --git a/libs/server/Resp/CmdStrings.cs b/libs/server/Resp/CmdStrings.cs index a427dc922a..245b26615c 100644 --- a/libs/server/Resp/CmdStrings.cs +++ b/libs/server/Resp/CmdStrings.cs @@ -219,6 +219,7 @@ static partial class CmdStrings public static ReadOnlySpan RESP_ERR_GENERIC_INDEX_OUT_RANGE => "ERR index out of range"u8; public static ReadOnlySpan RESP_ERR_GENERIC_SELECT_INVALID_INDEX => "ERR invalid database index."u8; public static ReadOnlySpan RESP_ERR_DB_INDEX_OUT_OF_RANGE => "ERR DB index is out of range."u8; + public static ReadOnlySpan RESP_ERR_DBSWAP_UNSUPPORTED => "ERR DBSWAP is currently unsupported when multiple clients are connected."u8; public static ReadOnlySpan RESP_ERR_GENERIC_SELECT_CLUSTER_MODE => "ERR SELECT is not allowed in cluster mode"u8; public static ReadOnlySpan RESP_ERR_NO_TRANSACTION_PROCEDURE => "ERR Could not get transaction procedure"u8; public static ReadOnlySpan RESP_ERR_WRONG_NUMBER_OF_ARGUMENTS => "ERR wrong number of arguments for command"u8; diff --git a/libs/server/Resp/GarnetDatabaseSession.cs b/libs/server/Resp/GarnetDatabaseSession.cs index e24e44b54e..b2a83453c0 100644 --- a/libs/server/Resp/GarnetDatabaseSession.cs +++ b/libs/server/Resp/GarnetDatabaseSession.cs @@ -16,8 +16,13 @@ namespace Garnet.server /* ObjectStoreFunctions */ StoreFunctions>, GenericAllocator>>>>; - internal struct GarnetDatabaseSession : IDisposable + internal struct GarnetDatabaseSession : IDefaultChecker, IDisposable { + /// + /// Database ID + /// + public int Id; + /// /// Storage session /// @@ -35,8 +40,9 @@ internal struct GarnetDatabaseSession : IDisposable bool disposed = false; - public GarnetDatabaseSession(StorageSession storageSession, BasicGarnetApi garnetApi, LockableGarnetApi lockableGarnetApi) + public GarnetDatabaseSession(int id, StorageSession storageSession, BasicGarnetApi garnetApi, LockableGarnetApi lockableGarnetApi) { + this.Id = id; this.StorageSession = storageSession; this.GarnetApi = garnetApi; this.LockableGarnetApi = lockableGarnetApi; @@ -51,5 +57,12 @@ public void Dispose() disposed = true; } + + public bool IsDefault() => StorageSession == null; + + /// + /// Instance of empty database session + /// + internal static GarnetDatabaseSession Empty; } } \ No newline at end of file diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index 581ede0b92..91352f9c0f 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -102,6 +102,8 @@ internal sealed unsafe partial class RespServerSession : ServerSessionBase readonly bool allowMultiDb; internal ExpandableMap databaseSessions; + GarnetDatabaseSession activeDatabaseSession; + /// /// The user currently authenticated in this session /// @@ -266,7 +268,7 @@ private GarnetDatabaseSession CreateDatabaseSession(int dbId) var dbStorageSession = new StorageSession(storeWrapper, scratchBufferManager, sessionMetrics, LatencyMetrics, logger, dbId); var dbGarnetApi = new BasicGarnetApi(dbStorageSession, dbStorageSession.basicContext, dbStorageSession.objectStoreBasicContext); var dbLockableGarnetApi = new LockableGarnetApi(dbStorageSession, dbStorageSession.lockableContext, dbStorageSession.objectStoreLockableContext); - return new GarnetDatabaseSession(dbStorageSession, dbGarnetApi, dbLockableGarnetApi); + return new GarnetDatabaseSession(dbId, dbStorageSession, dbGarnetApi, dbLockableGarnetApi); } internal void SetUserHandle(UserHandle userHandle) @@ -1307,8 +1309,8 @@ internal bool TrySwitchActiveDatabaseSession(int dbId) { if (!allowMultiDb) return false; - if (!databaseSessions.TryGetOrSet(dbId, () => CreateDatabaseSession(dbId), out var dbSession, out _)) - return false; + ref var dbSession = ref TryGetOrSetDatabaseSession(dbId, out var success); + if (!success) return false; SwitchActiveDatabaseSession(dbId, ref dbSession); return true; @@ -1319,24 +1321,66 @@ internal bool TrySwapDatabaseSessions(int dbId1, int dbId2) if (!allowMultiDb) return false; if (dbId1 == dbId2) return true; - if (!databaseSessions.TryGetOrSet(dbId1, () => CreateDatabaseSession(dbId2), out var dbSession1, out _) || - !databaseSessions.TryGetOrSet(dbId2, () => CreateDatabaseSession(dbId1), out var dbSession2, out _)) - return false; + ref var dbSession1 = ref TryGetOrSetDatabaseSession(dbId1, out var success, dbId2); + if (!success) return false; + ref var dbSession2 = ref TryGetOrSetDatabaseSession(dbId2, out success, dbId1); + if (!success) return false; + var tmp = dbSession1; databaseSessions.Map[dbId1] = dbSession2; - databaseSessions.Map[dbId2] = dbSession1; + databaseSessions.Map[dbId2] = tmp; if (activeDbId == dbId1) - SwitchActiveDatabaseSession(dbId1, ref dbSession2); + SwitchActiveDatabaseSession(dbId1, ref databaseSessions.Map[dbId1]); else if (activeDbId == dbId2) - SwitchActiveDatabaseSession(dbId2, ref dbSession1); + SwitchActiveDatabaseSession(dbId2, ref databaseSessions.Map[dbId2]); return true; } + private ref GarnetDatabaseSession TryGetOrSetDatabaseSession(int dbId, out bool success, int dbIdForSessionCreation = -1) + { + success = false; + if (dbIdForSessionCreation == -1) + dbIdForSessionCreation = dbId; + + var databaseSessionsMapSize = databaseSessions.ActualSize; + var databaseSessionsMapSnapshot = databaseSessions.Map; + + if (dbId >= 0 && dbId < databaseSessionsMapSize && !databaseSessionsMapSnapshot[dbId].IsDefault()) + { + success = true; + return ref databaseSessionsMapSnapshot[dbId]; + } + + databaseSessions.mapLock.WriteLock(); + + try + { + if (dbId >= 0 && dbId < databaseSessionsMapSize && !databaseSessionsMapSnapshot[dbId].IsDefault()) + { + success = true; + return ref databaseSessionsMapSnapshot[dbId]; + } + + var dbSession = CreateDatabaseSession(dbIdForSessionCreation); + if (!databaseSessions.TrySetValueUnsafe(dbId, ref dbSession, false)) + return ref GarnetDatabaseSession.Empty; + + success = true; + databaseSessionsMapSnapshot = databaseSessions.Map; + return ref databaseSessionsMapSnapshot[dbId]; + } + finally + { + databaseSessions.mapLock.WriteUnlock(); + } + } + private void SwitchActiveDatabaseSession(int dbId, ref GarnetDatabaseSession dbSession) { this.activeDbId = dbId; + this.activeDatabaseSession = dbSession; this.storageSession = dbSession.StorageSession; this.basicGarnetApi = dbSession.GarnetApi; this.lockableGarnetApi = dbSession.LockableGarnetApi; diff --git a/libs/server/Storage/Session/StorageSession.cs b/libs/server/Storage/Session/StorageSession.cs index 8db18f279b..c49125c945 100644 --- a/libs/server/Storage/Session/StorageSession.cs +++ b/libs/server/Storage/Session/StorageSession.cs @@ -73,7 +73,7 @@ public StorageSession(StoreWrapper storeWrapper, var functions = new MainSessionFunctions(functionsState); - var dbFound = storeWrapper.databaseManager.TryGetDatabase(dbId, out var db); + ref var db = ref storeWrapper.databaseManager.TryGetDatabase(dbId, out var dbFound); Debug.Assert(dbFound); var session = db.MainStore.NewSession(functions); diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 5ee268eea9..5bf981fcbb 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -78,7 +78,7 @@ public sealed class StoreWrapper /// /// Get server /// - public GarnetServerTcp TcpServer => (GarnetServerTcp)server; + public GarnetServerTcp TcpServer => server as GarnetServerTcp; /// /// Access control list governing all commands diff --git a/test/BDNPerfTests/BDN_Benchmark_Config.json b/test/BDNPerfTests/BDN_Benchmark_Config.json index da7782f563..8de6a83d69 100644 --- a/test/BDNPerfTests/BDN_Benchmark_Config.json +++ b/test/BDNPerfTests/BDN_Benchmark_Config.json @@ -383,5 +383,13 @@ "expected_Publish_ACL": 800, "expected_Publish_AOF": 800, "expected_Publish_None": 800 + }, + "BDN.benchmark.Operations.ServerOperations.*": { + "expected_SelectUnselect_ACL": 0, + "expected_SelectUnselect_AOF": 0, + "expected_SelectUnselect_None": 0, + "expected_SwapDb_ACL": 0, + "expected_SwapDb_AOF": 0, + "expected_SwapDb_None": 0 } } \ No newline at end of file diff --git a/test/Garnet.test/MultiDatabaseTests.cs b/test/Garnet.test/MultiDatabaseTests.cs index cc94556a08..7b07ce0309 100644 --- a/test/Garnet.test/MultiDatabaseTests.cs +++ b/test/Garnet.test/MultiDatabaseTests.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Garnet.common; +using Garnet.server; using NUnit.Framework; using NUnit.Framework.Legacy; using StackExchange.Redis; @@ -43,9 +44,12 @@ public void MultiDatabaseBasicSelectTestSE() ClassicAssert.IsFalse(db2.KeyExists(db1Key1)); ClassicAssert.IsFalse(db2.KeyExists(db1Key2)); - db2.StringSet(db2Key2, "db2:value2"); + db2.StringSet(db2Key1, "db2:value2"); db2.SetAdd(db2Key2, [new RedisValue("db2:val2"), new RedisValue("db2:val2")]); + ClassicAssert.IsTrue(db2.KeyExists(db2Key1)); + ClassicAssert.IsTrue(db2.KeyExists(db2Key2)); + ClassicAssert.IsFalse(db1.KeyExists(db2Key1)); ClassicAssert.IsFalse(db1.KeyExists(db2Key2)); @@ -368,6 +372,41 @@ public void MultiDatabaseSwapDatabasesTestLC() } [Test] + public void MultiDatabaseMultiSessionSwapDatabasesErrorTestLC() + { + // Ensure that SWAPDB returns an error when multiple clients are connected. + var db1Key1 = "db1:key1"; + var db2Key1 = "db2:key1"; + + using var lightClientRequest1 = TestUtils.CreateRequest(); // Session for DB 0 context + using var lightClientRequest2 = TestUtils.CreateRequest(); // Session for DB 1 context + + // Add data to DB 0 + var response = lightClientRequest1.SendCommand($"SET {db1Key1} db1:value1"); + var expectedResponse = "+OK\r\n"; + var actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + // Add data to DB 1 + response = lightClientRequest2.SendCommand($"SELECT 1"); + expectedResponse = "+OK\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest2.SendCommand($"SET {db2Key1} db2:value1"); + expectedResponse = "+OK\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + // Swap DB 0 AND DB 1 (from DB 0 context) + response = lightClientRequest1.SendCommand($"SWAPDB 0 1"); + expectedResponse = $"-{Encoding.ASCII.GetString(CmdStrings.RESP_ERR_DBSWAP_UNSUPPORTED)}\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + } + + [Test] + [Ignore("SWAPDB is currently disallowed for more than one client session. This test should be enabled once that changes.")] public void MultiDatabaseMultiSessionSwapDatabasesTestLC() { var db1Key1 = "db1:key1"; @@ -405,6 +444,11 @@ public void MultiDatabaseMultiSessionSwapDatabasesTestLC() actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); ClassicAssert.AreEqual(expectedResponse, actualValue); + response = lightClientRequest2.SendCommand($"GET {db2Key1}", 2); + expectedResponse = "$10\r\ndb2:value1\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + // Swap DB 0 AND DB 1 (from DB 0 context) response = lightClientRequest1.SendCommand($"SWAPDB 0 1"); expectedResponse = "+OK\r\n"; @@ -827,7 +871,7 @@ public void MultiDatabaseSaveRecoverByDbIdTest(bool backgroundSave) } expectedLastSave = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - Assert.That(lastSave, Is.InRange(expectedLastSave - 2, expectedLastSave + 2)); + Assert.That(lastSave, Is.InRange(expectedLastSave - 2, expectedLastSave)); // Verify DB 0 was not saved lastSaveStr = db1.Execute("LASTSAVE").ToString(); @@ -843,6 +887,10 @@ public void MultiDatabaseSaveRecoverByDbIdTest(bool backgroundSave) using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig())) { + var lastSave = 0L; + string lastSaveStr; + bool parsed; + // Verify that data was not recovered for DB 0 var db1 = redis.GetDatabase(0); @@ -879,13 +927,9 @@ public void MultiDatabaseSaveRecoverByDbIdTest(bool backgroundSave) ClassicAssert.AreEqual(backgroundSave ? "Background saving started" : "OK", res.ToString()); // Verify DB 0 was saved by checking LASTSAVE - var lastSave = 0L; - string lastSaveStr; - bool parsed; var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); while (!cts.IsCancellationRequested) { - // Verify DB 1 was saved by checking LASTSAVE lastSaveStr = db1.Execute("LASTSAVE").ToString(); parsed = long.TryParse(lastSaveStr, out lastSave); ClassicAssert.IsTrue(parsed); @@ -894,15 +938,16 @@ public void MultiDatabaseSaveRecoverByDbIdTest(bool backgroundSave) Task.Delay(TimeSpan.FromMilliseconds(100), cts.Token); } + var prevLastSave = expectedLastSave; expectedLastSave = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - Assert.That(lastSave, Is.InRange(expectedLastSave - 2, expectedLastSave + 2)); + Assert.That(lastSave, Is.InRange(expectedLastSave - 2, expectedLastSave)); // Verify DB 1 was not saved - Thread.Sleep(TimeSpan.FromSeconds(1)); + Thread.Sleep(TimeSpan.FromSeconds(2)); lastSaveStr = db1.Execute("LASTSAVE", "1").ToString(); parsed = long.TryParse(lastSaveStr, out lastSave); ClassicAssert.IsTrue(parsed); - ClassicAssert.AreEqual(0, lastSave); + Assert.That(lastSave, Is.InRange(prevLastSave - 2, prevLastSave)); } // Restart server From 1943070235129d74650a6a8eac7483fc290659c8 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 6 Mar 2025 20:18:54 -0800 Subject: [PATCH 75/82] Added website docs --- website/docs/commands/api-compatibility.md | 2 +- website/docs/commands/checkpoint.md | 12 ++++++------ website/docs/commands/garnet-specific.md | 5 ++--- website/docs/commands/server.md | 16 ++++++++++++++++ 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/website/docs/commands/api-compatibility.md b/website/docs/commands/api-compatibility.md index 118d0122b4..2bf3588421 100644 --- a/website/docs/commands/api-compatibility.md +++ b/website/docs/commands/api-compatibility.md @@ -291,7 +291,7 @@ Note that this list is subject to change as we continue to expand our API comman | | [SAVE](checkpoint.md#save) | ➕ | | | | SHUTDOWN | ➖ | | | | [SLAVEOF](server.md#slaveof) | ➕ | (Deprecated) | -| | SWAPDB | ➖ | | +| | [SWAPDB](server.md#swapdb) | ➕ | | | | SYNC | ➖ | | | | [TIME](server.md#time) | ➕ | | | **SET** | [SADD](data-structures.md#sadd) | ➕ | | diff --git a/website/docs/commands/checkpoint.md b/website/docs/commands/checkpoint.md index 39983f4059..c4b66c02e2 100644 --- a/website/docs/commands/checkpoint.md +++ b/website/docs/commands/checkpoint.md @@ -9,10 +9,10 @@ slug: checkpoint #### Syntax ```bash -BGSAVE [SCHEDULE] +BGSAVE [SCHEDULE] [DBID] ``` -Save the DB in background. +Save all databases inside the Garnet instance in the background. If a DB ID is specified, save save only that specific database. #### Resp Reply @@ -28,10 +28,10 @@ One of the following: #### Syntax ```bash -SAVE +SAVE [DBID] ``` -The SAVE commands performs a synchronous save of the dataset producing a point in time snapshot of all the data inside the Garnet instance. +The SAVE commands performs a synchronous save of the dataset producing a point in time snapshot of all the data inside the Garnet instance. If a DB ID is specified, only the data inside of that database will be snapshotted. #### Resp Reply @@ -42,10 +42,10 @@ Simple string reply: OK. #### Syntax ```bash -LASTSAVE +LASTSAVE [DBID] ``` -Return the UNIX TIME of the last DB save executed with success. +Return the UNIX TIME of the last DB save executed with success for the current database or, if a DB ID is specified, the last DB save executed with success for the specified database. #### Resp Reply diff --git a/website/docs/commands/garnet-specific.md b/website/docs/commands/garnet-specific.md index 699db52412..b4cd7b5445 100644 --- a/website/docs/commands/garnet-specific.md +++ b/website/docs/commands/garnet-specific.md @@ -30,11 +30,10 @@ Simple string reply: OK. #### Syntax ```bash - COMMITAOF + COMMITAOF [DBID] ``` -Issues a manual commit of the append-only-file. This is useful when auto-commits are turned off, but you need the -system to commit at specific times. +Issues a manual commit of the append-only-file (for all active databases in the Garnet instance). This is useful when auto-commits are turned off, but you need the system to commit at specific times. If a DB ID is specified, a manual commit of the append-only-file of that specific database will be issues. #### Resp Reply diff --git a/website/docs/commands/server.md b/website/docs/commands/server.md index 62ba3bdb3c..aa0ed624dd 100644 --- a/website/docs/commands/server.md +++ b/website/docs/commands/server.md @@ -361,6 +361,22 @@ Simple string reply: OK. --- +### SWAPDB + +#### Syntax + +```bash +SWAPDB index1 index2 +``` + +This command swaps two Garnet databases, so that immediately all the clients connected to a given database will see the data of the other database, and the other way around. + +#### Resp Reply + +Simple string reply: OK. + +--- + ### TIME #### Syntax From 4f49303e60f6a01705c322becf4350973e7f6cc8 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 6 Mar 2025 20:22:46 -0800 Subject: [PATCH 76/82] Fixing ACL test --- test/Garnet.test/Resp/ACL/RespCommandTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Garnet.test/Resp/ACL/RespCommandTests.cs b/test/Garnet.test/Resp/ACL/RespCommandTests.cs index 7084e57a1f..d33ff87529 100644 --- a/test/Garnet.test/Resp/ACL/RespCommandTests.cs +++ b/test/Garnet.test/Resp/ACL/RespCommandTests.cs @@ -7046,7 +7046,7 @@ await CheckCommandsAsync( static async Task DoSwapDbAsync(GarnetClient client) { - string val = await client.ExecuteForStringResultAsync("SWAPDB", ["1", "0"]); + string val = await client.ExecuteForStringResultAsync("SWAPDB", ["0", "0"]); ClassicAssert.AreEqual("OK", val); } } From 1b135d9dc6e66a276985a03665923ae5cdaa8ed2 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Fri, 7 Mar 2025 16:37:55 -0800 Subject: [PATCH 77/82] Initialize databaseManager as single database unless multi explicitly needed --- libs/server/AOF/AofProcessor.cs | 4 +- libs/server/Databases/DatabaseManagerBase.cs | 2 +- .../Databases/DatabaseManagerFactory.cs | 48 ++++- libs/server/Databases/MultiDatabaseManager.cs | 69 ++++---- libs/server/GarnetDatabase.cs | 4 +- libs/server/Metrics/GarnetServerMonitor.cs | 2 +- libs/server/Metrics/Info/GarnetInfoMetrics.cs | 16 +- libs/server/Resp/AdminCommands.cs | 5 +- libs/server/Resp/BasicCommands.cs | 4 +- libs/server/Resp/RespServerSession.cs | 2 +- libs/server/Servers/GarnetServerOptions.cs | 5 + libs/server/Servers/StoreApi.cs | 2 +- libs/server/Storage/Session/StorageSession.cs | 4 +- libs/server/StoreWrapper.cs | 166 +++++++++++++++++- 14 files changed, 265 insertions(+), 68 deletions(-) diff --git a/libs/server/AOF/AofProcessor.cs b/libs/server/AOF/AofProcessor.cs index 8068f09587..a357368e3c 100644 --- a/libs/server/AOF/AofProcessor.cs +++ b/libs/server/AOF/AofProcessor.cs @@ -201,14 +201,14 @@ public unsafe void ProcessAofRecordInternal(byte* ptr, int length, bool asReplic if (asReplica) { if (header.storeVersion > storeWrapper.store.CurrentVersion) - storeWrapper.databaseManager.TakeCheckpoint(false, StoreType.Main, logger: logger); + storeWrapper.TakeCheckpoint(false, StoreType.Main, logger: logger); } break; case AofEntryType.ObjectStoreCheckpointCommit: if (asReplica) { if (header.storeVersion > storeWrapper.objectStore.CurrentVersion) - storeWrapper.databaseManager.TakeCheckpoint(false, StoreType.Object, logger: logger); + storeWrapper.TakeCheckpoint(false, StoreType.Object, logger: logger); } break; case AofEntryType.MainStoreStreamingCheckpointCommit: diff --git a/libs/server/Databases/DatabaseManagerBase.cs b/libs/server/Databases/DatabaseManagerBase.cs index d76db78ae4..b42ab8d798 100644 --- a/libs/server/Databases/DatabaseManagerBase.cs +++ b/libs/server/Databases/DatabaseManagerBase.cs @@ -141,7 +141,7 @@ public abstract Task TaskCheckpointBasedOnAofSizeLimitAsync(long aofSizeLimit, /// /// Delegate for creating a new logical database /// - protected readonly StoreWrapper.DatabaseCreatorDelegate CreateDatabaseDelegate; + public readonly StoreWrapper.DatabaseCreatorDelegate CreateDatabaseDelegate; /// /// The main logger instance associated with the database manager. diff --git a/libs/server/Databases/DatabaseManagerFactory.cs b/libs/server/Databases/DatabaseManagerFactory.cs index 9702df29eb..7fe0771913 100644 --- a/libs/server/Databases/DatabaseManagerFactory.cs +++ b/libs/server/Databases/DatabaseManagerFactory.cs @@ -1,9 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using static Garnet.server.StoreWrapper; +using System.IO; +using System.Linq; -namespace Garnet.server.Databases +namespace Garnet.server { /// /// Factory class for creating new instances of IDatabaseManager @@ -19,11 +20,46 @@ public class DatabaseManagerFactory /// True if database manager should create a default database instance (default: true) /// public static IDatabaseManager CreateDatabaseManager(GarnetServerOptions serverOptions, - DatabaseCreatorDelegate createDatabaseDelegate, StoreWrapper storeWrapper, bool createDefaultDatabase = true) + StoreWrapper.DatabaseCreatorDelegate createDatabaseDelegate, StoreWrapper storeWrapper, bool createDefaultDatabase = true) { - return serverOptions.EnableCluster || serverOptions.MaxDatabases == 1 - ? new SingleDatabaseManager(createDatabaseDelegate, storeWrapper, createDefaultDatabase) - : new MultiDatabaseManager(createDatabaseDelegate, storeWrapper, createDefaultDatabase); + return ShouldCreateMultipleDatabaseManager(serverOptions, createDatabaseDelegate) ? + new MultiDatabaseManager(createDatabaseDelegate, storeWrapper, createDefaultDatabase) : + new SingleDatabaseManager(createDatabaseDelegate, storeWrapper, createDefaultDatabase); + } + + private static bool ShouldCreateMultipleDatabaseManager(GarnetServerOptions serverOptions, + StoreWrapper.DatabaseCreatorDelegate createDatabaseDelegate) + { + // If multiple databases are not allowed or recovery is disabled, create a single database manager + if (!serverOptions.AllowMultiDb || !serverOptions.Recover) + return false; + + // If there are multiple databases to recover, create a multi database manager, otherwise create a single database manager. + using (createDatabaseDelegate(0, out var checkpointDir, out var aofDir)) + { + // Check if there are multiple databases to recover from checkpoint + var checkpointDirInfo = new DirectoryInfo(checkpointDir); + var checkpointDirBaseName = checkpointDirInfo.Name; + var checkpointParentDir = checkpointDirInfo.Parent!.FullName; + + if (MultiDatabaseManager.TryGetSavedDatabaseIds(checkpointParentDir, checkpointDirBaseName, + out var dbIds) && dbIds.Any(id => id != 0)) + return true; + + // Check if there are multiple databases to recover from AOF + if (aofDir != null) + { + var aofDirInfo = new DirectoryInfo(aofDir); + var aofDirBaseName = aofDirInfo.Name; + var aofParentDir = aofDirInfo.Parent!.FullName; + + if (MultiDatabaseManager.TryGetSavedDatabaseIds(aofParentDir, aofDirBaseName, + out dbIds) && dbIds.Any(id => id != 0)) + return true; + } + + return false; + } } } } \ No newline at end of file diff --git a/libs/server/Databases/MultiDatabaseManager.cs b/libs/server/Databases/MultiDatabaseManager.cs index 056ab5aeba..5264ffb369 100644 --- a/libs/server/Databases/MultiDatabaseManager.cs +++ b/libs/server/Databases/MultiDatabaseManager.cs @@ -93,6 +93,12 @@ public MultiDatabaseManager(MultiDatabaseManager src, bool enableAof) : this(src CopyDatabases(src, enableAof); } + public MultiDatabaseManager(SingleDatabaseManager src) : + this(src.CreateDatabaseDelegate, src.StoreWrapper, false) + { + CopyDatabases(src, src.StoreWrapper.serverOptions.EnableAOF); + } + /// public override void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStoreFromToken = false, bool recoverObjectStoreFromToken = false, CheckpointMetadata metadata = null) { @@ -105,7 +111,6 @@ public override void RecoverCheckpoint(bool replicaRecover = false, bool recover { if (!TryGetSavedDatabaseIds(checkpointParentDir, checkpointDirBaseName, out dbIdsToRecover)) return; - } catch (Exception ex) { @@ -802,6 +807,37 @@ public async Task TryGetDatabasesWriteLockAsync(CancellationToken token = return lockAcquired; } + /// + /// Retrieves saved database IDs from parent checkpoint / AOF path + /// e.g. if path contains directories: baseName, baseName_1, baseName_2, baseName_10 + /// DB IDs 0,1,2,10 will be returned + /// + /// Parent path + /// Base name of directories containing database-specific checkpoints / AOFs + /// DB IDs extracted from parent path + /// True if successful + internal static bool TryGetSavedDatabaseIds(string path, string baseName, out int[] dbIds) + { + dbIds = default; + if (!Directory.Exists(path)) return false; + + var dirs = Directory.GetDirectories(path, $"{baseName}*", SearchOption.TopDirectoryOnly); + dbIds = new int[dirs.Length]; + for (var i = 0; i < dirs.Length; i++) + { + var dirName = new DirectoryInfo(dirs[i]).Name; + var sepIdx = dirName.IndexOf('_'); + var dbId = 0; + + if (sepIdx != -1 && !int.TryParse(dirName.AsSpan(sepIdx + 1), out dbId)) + continue; + + dbIds[i] = dbId; + } + + return true; + } + /// /// Try to add a new database /// @@ -873,37 +909,6 @@ private void HandleDatabaseAdded(int dbId) } } - /// - /// Retrieves saved database IDs from parent checkpoint / AOF path - /// e.g. if path contains directories: baseName, baseName_1, baseName_2, baseName_10 - /// DB IDs 0,1,2,10 will be returned - /// - /// Parent path - /// Base name of directories containing database-specific checkpoints / AOFs - /// DB IDs extracted from parent path - /// True if successful - private bool TryGetSavedDatabaseIds(string path, string baseName, out int[] dbIds) - { - dbIds = default; - if (!Directory.Exists(path)) return false; - - var dirs = Directory.GetDirectories(path, $"{baseName}*", SearchOption.TopDirectoryOnly); - dbIds = new int[dirs.Length]; - for (var i = 0; i < dirs.Length; i++) - { - var dirName = new DirectoryInfo(dirs[i]).Name; - var sepIdx = dirName.IndexOf('_'); - var dbId = 0; - - if (sepIdx != -1 && !int.TryParse(dirName.AsSpan(sepIdx + 1), out dbId)) - continue; - - dbIds[i] = dbId; - } - - return true; - } - /// /// Copy active databases from specified IDatabaseManager instance /// diff --git a/libs/server/GarnetDatabase.cs b/libs/server/GarnetDatabase.cs index 0c7496194c..21bf726de6 100644 --- a/libs/server/GarnetDatabase.cs +++ b/libs/server/GarnetDatabase.cs @@ -110,8 +110,8 @@ public GarnetDatabase(ref GarnetDatabase srcDb, bool enableAof) : this() MainStore = srcDb.MainStore; ObjectStore = srcDb.ObjectStore; ObjectStoreSizeTracker = srcDb.ObjectStoreSizeTracker; - AofDevice = enableAof ? AofDevice : null; - AppendOnlyFile = enableAof ? AppendOnlyFile : null; + AofDevice = enableAof ? srcDb.AofDevice : null; + AppendOnlyFile = enableAof ? srcDb.AppendOnlyFile : null; MainStoreIndexMaxedOut = srcDb.MainStoreIndexMaxedOut; ObjectStoreIndexMaxedOut = srcDb.ObjectStoreIndexMaxedOut; } diff --git a/libs/server/Metrics/GarnetServerMonitor.cs b/libs/server/Metrics/GarnetServerMonitor.cs index 7133552191..a27b41737b 100644 --- a/libs/server/Metrics/GarnetServerMonitor.cs +++ b/libs/server/Metrics/GarnetServerMonitor.cs @@ -193,7 +193,7 @@ private void ResetStats() storeWrapper.clusterProvider?.ResetGossipStats(); - storeWrapper.databaseManager.ResetRevivificationStats(); + storeWrapper.ResetRevivificationStats(); resetEventFlags[InfoMetricsType.STATS] = false; } diff --git a/libs/server/Metrics/Info/GarnetInfoMetrics.cs b/libs/server/Metrics/Info/GarnetInfoMetrics.cs index b6bc556c2b..7641a9ba58 100644 --- a/libs/server/Metrics/Info/GarnetInfoMetrics.cs +++ b/libs/server/Metrics/Info/GarnetInfoMetrics.cs @@ -78,7 +78,7 @@ private void PopulateMemoryInfo(StoreWrapper storeWrapper) var aof_log_memory_size = -1L; - var databases = storeWrapper.databaseManager.GetDatabasesSnapshot(); + var databases = storeWrapper.GetDatabasesSnapshot(); var disableObj = storeWrapper.serverOptions.DisableObjects; foreach (var db in databases) @@ -218,7 +218,7 @@ private void PopulateStatsInfo(StoreWrapper storeWrapper) private void PopulateStoreStats(StoreWrapper storeWrapper) { - var databases = storeWrapper.databaseManager.GetDatabasesSnapshot(); + var databases = storeWrapper.GetDatabasesSnapshot(); for (var i = 0; i < databases.Length; i++) { @@ -256,7 +256,7 @@ private MetricsItem[] GetDatabaseStoreStats(StoreWrapper storeWrapper, ref Garne private void PopulateObjectStoreStats(StoreWrapper storeWrapper) { - var databases = storeWrapper.databaseManager.GetDatabasesSnapshot(); + var databases = storeWrapper.GetDatabasesSnapshot(); for (var i = 0; i < databases.Length; i++) { @@ -294,7 +294,7 @@ private MetricsItem[] GetDatabaseObjectStoreStats(StoreWrapper storeWrapper, ref private void PopulateStoreHashDistribution(StoreWrapper storeWrapper) { - var databases = storeWrapper.databaseManager.GetDatabasesSnapshot(); + var databases = storeWrapper.GetDatabasesSnapshot(); var sb = new StringBuilder(); foreach (var db in databases) @@ -308,7 +308,7 @@ private void PopulateStoreHashDistribution(StoreWrapper storeWrapper) private void PopulateObjectStoreHashDistribution(StoreWrapper storeWrapper) { - var databases = storeWrapper.databaseManager.GetDatabasesSnapshot(); + var databases = storeWrapper.GetDatabasesSnapshot(); var sb = new StringBuilder(); foreach (var db in databases) @@ -322,7 +322,7 @@ private void PopulateObjectStoreHashDistribution(StoreWrapper storeWrapper) private void PopulateStoreRevivInfo(StoreWrapper storeWrapper) { - var databases = storeWrapper.databaseManager.GetDatabasesSnapshot(); + var databases = storeWrapper.GetDatabasesSnapshot(); var sb = new StringBuilder(); foreach (var db in databases) @@ -336,7 +336,7 @@ private void PopulateStoreRevivInfo(StoreWrapper storeWrapper) private void PopulateObjectStoreRevivInfo(StoreWrapper storeWrapper) { - var databases = storeWrapper.databaseManager.GetDatabasesSnapshot(); + var databases = storeWrapper.GetDatabasesSnapshot(); var sb = new StringBuilder(); foreach (var db in databases) { @@ -349,7 +349,7 @@ private void PopulateObjectStoreRevivInfo(StoreWrapper storeWrapper) private void PopulatePersistenceInfo(StoreWrapper storeWrapper) { - var databases = storeWrapper.databaseManager.GetDatabasesSnapshot(); + var databases = storeWrapper.GetDatabasesSnapshot(); for (var i = 0; i < databases.Length; i++) { diff --git a/libs/server/Resp/AdminCommands.cs b/libs/server/Resp/AdminCommands.cs index 2bf1562a51..0914e5e77b 100644 --- a/libs/server/Resp/AdminCommands.cs +++ b/libs/server/Resp/AdminCommands.cs @@ -934,8 +934,9 @@ private bool NetworkLASTSAVE() } } - ref var db = ref storeWrapper.databaseManager.TryGetOrAddDatabase(dbId, out var success, out _); - Debug.Assert(success); + var dbFound = storeWrapper.TryGetOrAddDatabase(dbId, out var db, out _); + Debug.Assert(dbFound); + var seconds = db.LastSaveTime.ToUnixTimeSeconds(); while (!RespWriteUtils.TryWriteInt64(seconds, ref dcurr, dend)) SendAndReset(); diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index 7574fdd1c5..1ed047099c 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -1682,10 +1682,10 @@ void ExecuteFlushDb(RespCommand cmd, bool unsafeTruncateLog) switch (cmd) { case RespCommand.FLUSHDB: - storeWrapper.databaseManager.FlushDatabase(unsafeTruncateLog, activeDbId); + storeWrapper.FlushDatabase(unsafeTruncateLog, activeDbId); break; case RespCommand.FLUSHALL: - storeWrapper.databaseManager.FlushAllDatabases(unsafeTruncateLog); + storeWrapper.FlushAllDatabases(unsafeTruncateLog); break; } } diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index 91352f9c0f..5b0803fb7d 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -230,7 +230,7 @@ public RespServerSession( var dbSession = CreateDatabaseSession(0); var maxDbs = storeWrapper.serverOptions.MaxDatabases; activeDbId = 0; - allowMultiDb = !storeWrapper.serverOptions.EnableCluster && maxDbs > 1; + allowMultiDb = storeWrapper.serverOptions.AllowMultiDb; databaseSessions = new ExpandableMap(1, 0, maxDbs - 1); if (!databaseSessions.TrySetValue(0, ref dbSession)) diff --git a/libs/server/Servers/GarnetServerOptions.cs b/libs/server/Servers/GarnetServerOptions.cs index 71c0bef124..d79a1f49fe 100644 --- a/libs/server/Servers/GarnetServerOptions.cs +++ b/libs/server/Servers/GarnetServerOptions.cs @@ -452,6 +452,11 @@ public class GarnetServerOptions : ServerOptions /// public int MaxDatabases = 16; + /// + /// Allow more than one logical database in server + /// + public bool AllowMultiDb => !EnableCluster && MaxDatabases > 1; + /// /// Constructor /// diff --git a/libs/server/Servers/StoreApi.cs b/libs/server/Servers/StoreApi.cs index cdbdf28a48..c0cbc3076e 100644 --- a/libs/server/Servers/StoreApi.cs +++ b/libs/server/Servers/StoreApi.cs @@ -49,6 +49,6 @@ public StoreApi(StoreWrapper storeWrapper) /// that will safely truncate the log on disk after the checkpoint. /// public void FlushDB(int dbId = 0, bool unsafeTruncateLog = false) => - storeWrapper.databaseManager.FlushDatabase(unsafeTruncateLog, dbId); + storeWrapper.FlushDatabase(unsafeTruncateLog, dbId); } } \ No newline at end of file diff --git a/libs/server/Storage/Session/StorageSession.cs b/libs/server/Storage/Session/StorageSession.cs index c49125c945..86c2437c14 100644 --- a/libs/server/Storage/Session/StorageSession.cs +++ b/libs/server/Storage/Session/StorageSession.cs @@ -69,11 +69,11 @@ public StorageSession(StoreWrapper storeWrapper, parseState.Initialize(); - functionsState = storeWrapper.databaseManager.CreateFunctionsState(dbId); + functionsState = storeWrapper.CreateFunctionsState(dbId); var functions = new MainSessionFunctions(functionsState); - ref var db = ref storeWrapper.databaseManager.TryGetDatabase(dbId, out var dbFound); + var dbFound = storeWrapper.TryGetDatabase(dbId, out var db); Debug.Assert(dbFound); var session = db.MainStore.NewSession(functions); diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index 5bf981fcbb..4e1bb28f2a 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -12,7 +12,6 @@ using Garnet.common; using Garnet.server.ACL; using Garnet.server.Auth.Settings; -using Garnet.server.Databases; using Garnet.server.Lua; using Microsoft.Extensions.Logging; using Tsavorite.core; @@ -125,7 +124,8 @@ public sealed class StoreWrapper /// internal readonly LuaTimeoutManager luaTimeoutManager; - internal readonly IDatabaseManager databaseManager; + private IDatabaseManager databaseManager; + SingleWriterMultiReaderLock databaseManagerLock; internal readonly CollectionItemBroker itemBroker; internal readonly CustomCommandManager customCommandManager; @@ -302,6 +302,17 @@ internal void Recover() } } + /// + /// Take checkpoint of all active databases + /// + /// True if method can return before checkpoint is taken + /// Store type to checkpoint + /// Logger + /// Cancellation token + /// False if another checkpointing process is already in progress + public bool TakeCheckpoint(bool background, StoreType storeType = StoreType.All, ILogger logger = null, + CancellationToken token = default) => databaseManager.TakeCheckpoint(background, storeType, logger, token); + /// /// Recover checkpoint /// @@ -326,14 +337,105 @@ public void RecoverCheckpoint(bool replicaRecover = false, bool recoverMainStore /// /// /// - public void EnqueueCommit(bool isMainStore, long version, int dbId = 0, bool diskless = false) => + public void EnqueueCommit(bool isMainStore, long version, int dbId = 0, bool diskless = false) + { + if (dbId != 0 && !CheckMultiDatabaseCompatibility()) + throw new GarnetException($"Unable to call {nameof(databaseManager.EnqueueCommit)} with DB ID: {dbId}"); + this.databaseManager.EnqueueCommit(isMainStore, version, dbId, diskless); + } + + internal FunctionsState CreateFunctionsState(int dbId = 0) + { + if (dbId != 0 && !CheckMultiDatabaseCompatibility()) + throw new GarnetException($"Unable to call {nameof(databaseManager.CreateFunctionsState)} with DB ID: {dbId}"); + + return databaseManager.CreateFunctionsState(dbId); + } /// /// Reset /// /// Database ID - public void Reset(int dbId = 0) => databaseManager.Reset(dbId); + public void Reset(int dbId = 0) + { + if (dbId != 0 && !CheckMultiDatabaseCompatibility()) + throw new GarnetException($"Unable to call {nameof(databaseManager.Reset)} with DB ID: {dbId}"); + + databaseManager.Reset(dbId); + } + + /// + /// Resets the revivification stats. + /// + public void ResetRevivificationStats() => databaseManager.ResetRevivificationStats(); + + /// + /// Get a snapshot of all active databases + /// + /// Array of active databases + public GarnetDatabase[] GetDatabasesSnapshot() => databaseManager.GetDatabasesSnapshot(); + + /// + /// Get database DB ID + /// + /// DB Id + /// Retrieved database + /// True if database was found + public bool TryGetDatabase(int dbId, out GarnetDatabase database) + { + + if (dbId != 0 && !CheckMultiDatabaseCompatibility()) + { + database = GarnetDatabase.Empty; + return false; + } + + database = databaseManager.TryGetDatabase(dbId, out var success); + return success; + } + + /// + /// Try to get or add a new database + /// + /// Database ID + /// Retrieved or added database + /// True if database was added + /// True if database was found or added + public bool TryGetOrAddDatabase(int dbId, out GarnetDatabase database, out bool added) + { + if (dbId != 0 && !CheckMultiDatabaseCompatibility()) + { + database = GarnetDatabase.Empty; + added = false; + return false; + } + + database = databaseManager.TryGetOrAddDatabase(dbId, out var success, out added); + return success; + } + + /// + /// Flush database with specified ID + /// + /// Truncate log + /// Database ID + public void FlushDatabase(bool unsafeTruncateLog, int dbId = 0) + { + if (dbId != 0 && !CheckMultiDatabaseCompatibility()) + throw new GarnetException($"Unable to call {nameof(databaseManager.FlushDatabase)} with DB ID: {dbId}"); + + databaseManager.FlushDatabase(unsafeTruncateLog, dbId); + } + + /// + /// Flush all active databases + /// + /// Truncate log + public void FlushAllDatabases(bool unsafeTruncateLog) + { + databaseManager.FlushAllDatabases(unsafeTruncateLog); + } /// /// Try to swap between two database instances @@ -341,7 +443,12 @@ public void EnqueueCommit(bool isMainStore, long version, int dbId = 0, bool dis /// First database ID /// Second database ID /// True if swap successful - public bool TrySwapDatabases(int dbId1, int dbId2) => this.databaseManager.TrySwapDatabases(dbId1, dbId2); + public bool TrySwapDatabases(int dbId1, int dbId2) + { + if (databaseManager is SingleDatabaseManager) return false; + + return this.databaseManager.TrySwapDatabases(dbId1, dbId2); + } async Task AutoCheckpointBasedOnAofSizeLimit(long aofSizeLimit, CancellationToken token = default, ILogger logger = null) { @@ -505,6 +612,9 @@ internal async ValueTask CommitAOFAsync(int dbId = -1, CancellationToken token = return; } + if (dbId != 0 && !CheckMultiDatabaseCompatibility()) + throw new GarnetException($"Unable to call {nameof(databaseManager.CommitToAofAsync)} with DB ID: {dbId}"); + await databaseManager.CommitToAofAsync(dbId, token); } @@ -524,6 +634,9 @@ public bool TakeCheckpoint(bool background, int dbId = -1, StoreType storeType = return databaseManager.TakeCheckpoint(background, storeType, logger, token); } + if (dbId != 0 && !CheckMultiDatabaseCompatibility()) + throw new GarnetException($"Unable to call {nameof(databaseManager.TakeCheckpoint)} with DB ID: {dbId}"); + return databaseManager.TakeCheckpoint(background, dbId, storeType, logger, token); } @@ -609,14 +722,24 @@ public void Dispose() /// ID of database to lock /// True if lock acquired public bool TryPauseCheckpoints(int dbId = 0) - => databaseManager.TryPauseCheckpoints(dbId); + { + if (dbId != 0 && !CheckMultiDatabaseCompatibility()) + throw new GarnetException($"Unable to call {nameof(databaseManager.TryPauseCheckpoints)} with DB ID: {dbId}"); + + return databaseManager.TryPauseCheckpoints(dbId); + } /// /// Release checkpoint task lock /// /// ID of database to unlock public void ResumeCheckpoints(int dbId = 0) - => databaseManager.ResumeCheckpoints(dbId); + { + if (dbId != 0 && !CheckMultiDatabaseCompatibility()) + throw new GarnetException($"Unable to call {nameof(databaseManager.ResumeCheckpoints)} with DB ID: {dbId}"); + + databaseManager.ResumeCheckpoints(dbId); + } /// /// Take a checkpoint if no checkpoint was taken after the provided time offset @@ -624,8 +747,13 @@ public void ResumeCheckpoints(int dbId = 0) /// /// /// - public async Task TakeOnDemandCheckpoint(DateTimeOffset entryTime, int dbId = 0) => + public async Task TakeOnDemandCheckpoint(DateTimeOffset entryTime, int dbId = 0) + { + if (dbId != 0 && !CheckMultiDatabaseCompatibility()) + throw new GarnetException($"Unable to call {nameof(databaseManager.TakeOnDemandCheckpointAsync)} with DB ID: {dbId}"); + await databaseManager.TakeOnDemandCheckpointAsync(entryTime, dbId); + } public bool HasKeysInSlots(List slots) { @@ -667,5 +795,27 @@ public bool HasKeysInSlots(List slots) return false; } + + private bool CheckMultiDatabaseCompatibility() + { + if (databaseManager is MultiDatabaseManager) + return true; + + if (!serverOptions.AllowMultiDb) + return false; + + databaseManagerLock.WriteLock(); + try + { + if (databaseManager is SingleDatabaseManager singleDatabaseManager) + databaseManager = new MultiDatabaseManager(singleDatabaseManager); + + return true; + } + finally + { + databaseManagerLock.WriteUnlock(); + } + } } } \ No newline at end of file From fbecd3370275f36fd9b7e0c25589b074a93d46f6 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Fri, 7 Mar 2025 17:20:23 -0800 Subject: [PATCH 78/82] merge fix + format --- libs/host/GarnetServer.cs | 4 ++-- libs/server/Databases/DatabaseManagerBase.cs | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/libs/host/GarnetServer.cs b/libs/host/GarnetServer.cs index 170581faaf..cc0f20483d 100644 --- a/libs/host/GarnetServer.cs +++ b/libs/host/GarnetServer.cs @@ -342,8 +342,8 @@ private TsavoriteKV Date: Fri, 7 Mar 2025 17:22:44 -0800 Subject: [PATCH 79/82] merging from main + removing unnecessary usings --- libs/server/Databases/DatabaseManagerBase.cs | 1 - libs/server/Resp/BasicCommands.cs | 5 ----- libs/server/Servers/GarnetServerOptions.cs | 1 - 3 files changed, 7 deletions(-) diff --git a/libs/server/Databases/DatabaseManagerBase.cs b/libs/server/Databases/DatabaseManagerBase.cs index 0926ebd5cb..faea42cc9b 100644 --- a/libs/server/Databases/DatabaseManagerBase.cs +++ b/libs/server/Databases/DatabaseManagerBase.cs @@ -9,7 +9,6 @@ namespace Garnet.server { - using static System.Formats.Asn1.AsnWriter; using MainStoreAllocator = SpanByteAllocator>; using MainStoreFunctions = StoreFunctions; diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index 374e251067..e34d2998c4 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -12,11 +12,6 @@ namespace Garnet.server { - using MainStoreAllocator = SpanByteAllocator>; - using MainStoreFunctions = StoreFunctions; - using ObjectStoreAllocator = GenericAllocator>>; - using ObjectStoreFunctions = StoreFunctions>; - /// /// Server session for RESP protocol - basic commands are in this file /// diff --git a/libs/server/Servers/GarnetServerOptions.cs b/libs/server/Servers/GarnetServerOptions.cs index d79a1f49fe..ef791ae4e6 100644 --- a/libs/server/Servers/GarnetServerOptions.cs +++ b/libs/server/Servers/GarnetServerOptions.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.IO; -using Garnet.common; using Garnet.server.Auth.Settings; using Garnet.server.TLS; using Microsoft.Extensions.Logging; From cb2962280087aa097f99c6f95c3c8a4416fdbf07 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Fri, 7 Mar 2025 18:03:46 -0800 Subject: [PATCH 80/82] bugfix --- libs/server/GarnetDatabase.cs | 8 ++++++-- libs/server/Storage/SizeTracker/CacheSizeTracker.cs | 12 +++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/libs/server/GarnetDatabase.cs b/libs/server/GarnetDatabase.cs index 21bf726de6..0373a59c9b 100644 --- a/libs/server/GarnetDatabase.cs +++ b/libs/server/GarnetDatabase.cs @@ -140,8 +140,12 @@ public void Dispose() if (ObjectStoreSizeTracker != null) { - while (!ObjectStoreSizeTracker.Stopped) - Thread.Yield(); + // If tracker has previously started, wait for it to stop + if (!ObjectStoreSizeTracker.TryPreventStart()) + { + while (!ObjectStoreSizeTracker.Stopped) + Thread.Yield(); + } } disposed = true; diff --git a/libs/server/Storage/SizeTracker/CacheSizeTracker.cs b/libs/server/Storage/SizeTracker/CacheSizeTracker.cs index be2b7cc9ef..fe226a500f 100644 --- a/libs/server/Storage/SizeTracker/CacheSizeTracker.cs +++ b/libs/server/Storage/SizeTracker/CacheSizeTracker.cs @@ -27,7 +27,7 @@ public class CacheSizeTracker int isStarted = 0; private const int deltaFraction = 10; // 10% of target size - private TsavoriteKV store; + TsavoriteKV store; internal bool Stopped => (mainLogTracker == null || mainLogTracker.Stopped) && (readCacheTracker == null || readCacheTracker.Stopped); @@ -118,5 +118,15 @@ public void AddReadCacheTrackedSize(long size) // just for the main log this.readCacheTracker?.IncrementSize(size); } + + /// + /// If tracker has not started, prevent it from starting + /// + /// True if tracker hasn't previously started + public bool TryPreventStart() + { + var prevStarted = Interlocked.CompareExchange(ref isStarted, 1, 0); + return prevStarted == 0; + } } } \ No newline at end of file From 6e9d4c8edacd775d0a18705560719060d8c6cc9c Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Fri, 7 Mar 2025 19:05:52 -0800 Subject: [PATCH 81/82] Test fix + consolidation of getting directory paths for checkpointing and AOF --- libs/host/GarnetServer.cs | 30 +++---- .../Databases/DatabaseManagerFactory.cs | 15 ++-- libs/server/Databases/MultiDatabaseManager.cs | 31 ++------ .../server/Databases/SingleDatabaseManager.cs | 2 +- libs/server/Servers/GarnetServerOptions.cs | 79 +++++++++++++++---- libs/server/StoreWrapper.cs | 2 +- test/Garnet.test/Resp/ACL/RespCommandTests.cs | 15 +++- 7 files changed, 103 insertions(+), 71 deletions(-) diff --git a/libs/host/GarnetServer.cs b/libs/host/GarnetServer.cs index cc0f20483d..296ef10b2c 100644 --- a/libs/host/GarnetServer.cs +++ b/libs/host/GarnetServer.cs @@ -223,8 +223,8 @@ private void InitializeServer() if (!setMax && !ThreadPool.SetMaxThreads(maxThreads, maxCPThreads)) throw new Exception($"Unable to call ThreadPool.SetMaxThreads with {maxThreads}, {maxCPThreads}"); - StoreWrapper.DatabaseCreatorDelegate createDatabaseDelegate = (int dbId, out string storeCheckpointDir, out string aofDir) => - CreateDatabase(dbId, opts, clusterFactory, customCommandManager, out storeCheckpointDir, out aofDir); + StoreWrapper.DatabaseCreatorDelegate createDatabaseDelegate = (int dbId) => + CreateDatabase(dbId, opts, clusterFactory, customCommandManager); if (!opts.DisablePubSub) subscribeBroker = new SubscribeBroker(null, opts.PubSubPageSizeBytes(), opts.SubscriberRefreshFrequencyMs, true, logger); @@ -276,12 +276,11 @@ private void InitializeServer() } private GarnetDatabase CreateDatabase(int dbId, GarnetServerOptions serverOptions, ClusterFactory clusterFactory, - CustomCommandManager customCommandManager, out string storeCheckpointDir, out string aofDir) + CustomCommandManager customCommandManager) { - var store = CreateMainStore(dbId, clusterFactory, out var checkpointDir, out storeCheckpointDir); - var objectStore = CreateObjectStore(dbId, clusterFactory, customCommandManager, checkpointDir, - out var objectStoreSizeTracker); - var (aofDevice, aof) = CreateAOF(dbId, out aofDir); + var store = CreateMainStore(dbId, clusterFactory); + var objectStore = CreateObjectStore(dbId, clusterFactory, customCommandManager, out var objectStoreSizeTracker); + var (aofDevice, aof) = CreateAOF(dbId); return new GarnetDatabase(dbId, store, objectStore, objectStoreSizeTracker, aofDevice, aof, serverOptions.AdjustedIndexMaxCacheLines == 0, serverOptions.AdjustedObjectStoreIndexMaxCacheLines == 0); @@ -311,18 +310,15 @@ private void LoadModules(CustomCommandManager customCommandManager) } } - private TsavoriteKV CreateMainStore(int dbId, IClusterFactory clusterFactory, out string checkpointDir, out string mainStoreCheckpointDir) + private TsavoriteKV CreateMainStore(int dbId, IClusterFactory clusterFactory) { kvSettings = opts.GetSettings(loggerFactory, out logFactory); - checkpointDir = (opts.CheckpointDir ?? opts.LogDir) ?? string.Empty; - // Run checkpoint on its own thread to control p99 kvSettings.ThrottleCheckpointFlushDelayMs = opts.CheckpointThrottleFlushDelayMs; - var baseName = Path.Combine(checkpointDir, "Store", $"checkpoints{(dbId == 0 ? string.Empty : $"_{dbId}")}"); + var baseName = opts.GetMainStoreCheckpointDirectory(dbId); var defaultNamingScheme = new DefaultCheckpointNamingScheme(baseName); - mainStoreCheckpointDir = baseName; kvSettings.CheckpointManager = opts.EnableCluster ? clusterFactory.CreateCheckpointManager(opts.DeviceFactoryCreator, defaultNamingScheme, isMainStore: true, logger) : @@ -333,7 +329,7 @@ private TsavoriteKV , (allocatorSettings, storeFunctions) => new(allocatorSettings, storeFunctions)); } - private TsavoriteKV CreateObjectStore(int dbId, IClusterFactory clusterFactory, CustomCommandManager customCommandManager, string checkpointDir, out CacheSizeTracker objectStoreSizeTracker) + private TsavoriteKV CreateObjectStore(int dbId, IClusterFactory clusterFactory, CustomCommandManager customCommandManager, out CacheSizeTracker objectStoreSizeTracker) { objectStoreSizeTracker = null; if (opts.DisableObjects) @@ -345,7 +341,7 @@ private TsavoriteKV id != 0)) return true; // Check if there are multiple databases to recover from AOF - if (aofDir != null) + if (serverOptions.EnableAOF) { - var aofDirInfo = new DirectoryInfo(aofDir); - var aofDirBaseName = aofDirInfo.Name; - var aofParentDir = aofDirInfo.Parent!.FullName; + var aofParentDir = serverOptions.AppendOnlyFileBaseDirectory; + var aofDirBaseName = serverOptions.GetAppendOnlyFileDirectoryName(0); if (MultiDatabaseManager.TryGetSavedDatabaseIds(aofParentDir, aofDirBaseName, out dbIds) && dbIds.Any(id => id != 0)) diff --git a/libs/server/Databases/MultiDatabaseManager.cs b/libs/server/Databases/MultiDatabaseManager.cs index bcf66b7fbd..9bfb2a7aa3 100644 --- a/libs/server/Databases/MultiDatabaseManager.cs +++ b/libs/server/Databases/MultiDatabaseManager.cs @@ -46,16 +46,6 @@ internal class MultiDatabaseManager : DatabaseManagerBase // Reusable array for storing database IDs for checkpointing int[] dbIdsToCheckpoint; - // Path of serialization for the DB IDs file used when committing / recovering to / from AOF - readonly string aofParentDir; - - readonly string aofDirBaseName; - - // Path of serialization for the DB IDs file used when committing / recovering to / from a checkpoint - readonly string checkpointParentDir; - - readonly string checkpointDirBaseName; - public MultiDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createDatabaseDelegate, StoreWrapper storeWrapper, bool createDefaultDatabase = true) : base(createDatabaseDelegate, storeWrapper) { @@ -68,18 +58,7 @@ public MultiDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createDatabaseD // Create default database of index 0 (unless specified otherwise) if (createDefaultDatabase) { - var db = createDatabaseDelegate(0, out var storeCheckpointDir, out var aofDir); - - var checkpointDirInfo = new DirectoryInfo(storeCheckpointDir); - checkpointDirBaseName = checkpointDirInfo.Name; - checkpointParentDir = checkpointDirInfo.Parent!.FullName; - - if (aofDir != null) - { - var aofDirInfo = new DirectoryInfo(aofDir); - aofDirBaseName = aofDirInfo.Name; - aofParentDir = aofDirInfo.Parent!.FullName; - } + var db = createDatabaseDelegate(0); // Set new database in map if (!TryAddDatabase(0, ref db)) @@ -106,6 +85,9 @@ public override void RecoverCheckpoint(bool replicaRecover = false, bool recover throw new GarnetException( $"Unexpected call to {nameof(MultiDatabaseManager)}.{nameof(RecoverCheckpoint)} with {nameof(replicaRecover)} == true."); + var checkpointParentDir = StoreWrapper.serverOptions.MainStoreCheckpointBaseDirectory; + var checkpointDirBaseName = StoreWrapper.serverOptions.GetCheckpointDirectoryName(0); + int[] dbIdsToRecover; try { @@ -367,6 +349,9 @@ public override async Task WaitForCommitToAofAsync(CancellationToken token = def /// public override void RecoverAOF() { + var aofParentDir = StoreWrapper.serverOptions.AppendOnlyFileBaseDirectory; + var aofDirBaseName = StoreWrapper.serverOptions.GetAppendOnlyFileDirectoryName(0); + int[] dbIdsToRecover; try { @@ -662,7 +647,7 @@ public override ref GarnetDatabase TryGetOrAddDatabase(int dbId, out bool succes return ref databasesMapSnapshot[dbId]; } - var db = CreateDatabaseDelegate(dbId, out _, out _); + var db = CreateDatabaseDelegate(dbId); if (!databases.TrySetValueUnsafe(dbId, ref db, false)) return ref GarnetDatabase.Empty; } diff --git a/libs/server/Databases/SingleDatabaseManager.cs b/libs/server/Databases/SingleDatabaseManager.cs index d08451ce83..a94355c2a7 100644 --- a/libs/server/Databases/SingleDatabaseManager.cs +++ b/libs/server/Databases/SingleDatabaseManager.cs @@ -27,7 +27,7 @@ public SingleDatabaseManager(StoreWrapper.DatabaseCreatorDelegate createDatabase // Create default database of index 0 (unless specified otherwise) if (createDefaultDatabase) { - defaultDatabase = createDatabaseDelegate(0, out _, out _); + defaultDatabase = createDatabaseDelegate(0); } } diff --git a/libs/server/Servers/GarnetServerOptions.cs b/libs/server/Servers/GarnetServerOptions.cs index ef791ae4e6..9ac51eb926 100644 --- a/libs/server/Servers/GarnetServerOptions.cs +++ b/libs/server/Servers/GarnetServerOptions.cs @@ -456,6 +456,64 @@ public class GarnetServerOptions : ServerOptions /// public bool AllowMultiDb => !EnableCluster && MaxDatabases > 1; + /// + /// Gets the base directory for storing checkpoints + /// + public string CheckpointBaseDirectory => (CheckpointDir ?? LogDir) ?? string.Empty; + + /// + /// Gets the base directory for storing main-store checkpoints + /// + public string MainStoreCheckpointBaseDirectory => Path.Combine(CheckpointBaseDirectory, "Store"); + + /// + /// Gets the base directory for storing object-store checkpoints + /// + public string ObjectStoreCheckpointBaseDirectory => Path.Combine(CheckpointBaseDirectory, "ObjectStore"); + + /// + /// Get the directory name for database checkpoints + /// + /// Database Id + /// Directory name + public string GetCheckpointDirectoryName(int dbId) => $"checkpoints{(dbId == 0 ? string.Empty : $"_{dbId}")}"; + + /// + /// Get the directory for main-store database checkpoints + /// + /// Database Id + /// Directory + public string GetMainStoreCheckpointDirectory(int dbId) => + Path.Combine(MainStoreCheckpointBaseDirectory, GetCheckpointDirectoryName(dbId)); + + /// + /// Get the directory for object-store database checkpoints + /// + /// Database Id + /// Directory + public string GetObjectStoreCheckpointDirectory(int dbId) => + Path.Combine(ObjectStoreCheckpointBaseDirectory, GetCheckpointDirectoryName(dbId)); + + /// + /// Gets the base directory for storing AOF commits + /// + public string AppendOnlyFileBaseDirectory => CheckpointDir ?? string.Empty; + + /// + /// Get the directory name for database AOF commits + /// + /// Database Id + /// Directory name + public string GetAppendOnlyFileDirectoryName(int dbId) => $"AOF{(dbId == 0 ? string.Empty : $"_{dbId}")}"; + + /// + /// Get the directory for database AOF commits + /// + /// Database Id + /// Directory + public string GetAppendOnlyFileDirectory(int dbId) => + Path.Combine(AppendOnlyFileBaseDirectory, GetAppendOnlyFileDirectoryName(dbId)); + /// /// Constructor /// @@ -742,8 +800,7 @@ public KVSettings GetObjectStoreSettings(ILogger logger, /// /// DB ID /// Tsavorite log settings - /// - public void GetAofSettings(int dbId, out TsavoriteLogSettings tsavoriteLogSettings, out string aofDir) + public void GetAofSettings(int dbId, out TsavoriteLogSettings tsavoriteLogSettings) { tsavoriteLogSettings = new TsavoriteLogSettings { @@ -762,7 +819,7 @@ public void GetAofSettings(int dbId, out TsavoriteLogSettings tsavoriteLogSettin throw new Exception("AOF Page size cannot be more than the AOF memory size."); } - aofDir = Path.Combine(CheckpointDir ?? string.Empty, $"AOF{(dbId == 0 ? string.Empty : $"_{dbId}")}"); + var aofDir = GetAppendOnlyFileDirectory(dbId); tsavoriteLogSettings.LogCommitManager = new DeviceLogCommitCheckpointManager( FastAofTruncate ? new NullNamedDeviceFactoryCreator() : DeviceFactoryCreator, new DefaultCheckpointNamingScheme(aofDir), @@ -855,20 +912,8 @@ IDevice GetAofDevice(int dbId) throw new Exception("Cannot use null device for AOF when cluster is enabled and you are not using main memory replication"); if (UseAofNullDevice) return new NullDevice(); - return GetInitializedDeviceFactory(CheckpointDir) - .Get(new FileDescriptor($"AOF{(dbId == 0 ? string.Empty : $"_{dbId}")}", "aof.log")); - } - - /// - /// Get device for logging database IDs - /// - /// - public IDevice GetDatabaseIdsDevice() - { - if (MaxDatabases == 1) return new NullDevice(); - - return GetInitializedDeviceFactory(CheckpointDir) - .Get(new FileDescriptor($"databases", "ids.dat")); + return GetInitializedDeviceFactory(AppendOnlyFileBaseDirectory) + .Get(new FileDescriptor(GetAppendOnlyFileDirectoryName(dbId), "aof.log")); } } } \ No newline at end of file diff --git a/libs/server/StoreWrapper.cs b/libs/server/StoreWrapper.cs index ebc96938a9..13463de779 100644 --- a/libs/server/StoreWrapper.cs +++ b/libs/server/StoreWrapper.cs @@ -112,7 +112,7 @@ public sealed class StoreWrapper /// /// Definition for delegate creating a new logical database /// - public delegate GarnetDatabase DatabaseCreatorDelegate(int dbId, out string storeCheckpointDir, out string aofDir); + public delegate GarnetDatabase DatabaseCreatorDelegate(int dbId); /// /// Number of active databases diff --git a/test/Garnet.test/Resp/ACL/RespCommandTests.cs b/test/Garnet.test/Resp/ACL/RespCommandTests.cs index c3e6507db2..2ebff69f04 100644 --- a/test/Garnet.test/Resp/ACL/RespCommandTests.cs +++ b/test/Garnet.test/Resp/ACL/RespCommandTests.cs @@ -7054,8 +7054,19 @@ await CheckCommandsAsync( static async Task DoSwapDbAsync(GarnetClient client) { - string val = await client.ExecuteForStringResultAsync("SWAPDB", ["0", "0"]); - ClassicAssert.AreEqual("OK", val); + try + { + // Currently SWAPDB does not support calling the command when multiple clients are connected to the server. + await client.ExecuteForStringResultAsync("SWAPDB", ["0", "1"]); + Assert.Fail("Shouldn't reach here, calling SWAPDB should fail."); + } + catch (Exception ex) + { + if (ex.Message == Encoding.ASCII.GetString(CmdStrings.RESP_ERR_NOAUTH)) + throw; + + ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_DBSWAP_UNSUPPORTED), ex.Message); + } } } From 62159e6b6ff759f240f178c0b58cc1170370e7b1 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Fri, 7 Mar 2025 19:14:11 -0800 Subject: [PATCH 82/82] Updating DB IDs after swap --- libs/server/Databases/MultiDatabaseManager.cs | 3 +++ libs/server/Resp/RespServerSession.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/libs/server/Databases/MultiDatabaseManager.cs b/libs/server/Databases/MultiDatabaseManager.cs index 9bfb2a7aa3..67ef79a15c 100644 --- a/libs/server/Databases/MultiDatabaseManager.cs +++ b/libs/server/Databases/MultiDatabaseManager.cs @@ -581,6 +581,9 @@ public override bool TrySwapDatabases(int dbId1, int dbId2) databaseMapSnapshot[dbId1] = db2; databaseMapSnapshot[dbId2] = tmp; + databaseMapSnapshot[dbId1].Id = dbId1; + databaseMapSnapshot[dbId2].Id = dbId2; + var sessions = StoreWrapper.TcpServer?.ActiveConsumers().ToArray(); if (sessions == null) return true; if (sessions.Length > 1) return false; diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index e4b2cb20fd..19c160e729 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -1330,6 +1330,9 @@ internal bool TrySwapDatabaseSessions(int dbId1, int dbId2) databaseSessions.Map[dbId1] = dbSession2; databaseSessions.Map[dbId2] = tmp; + databaseSessions.Map[dbId1].Id = dbId1; + databaseSessions.Map[dbId2].Id = dbId2; + if (activeDbId == dbId1) SwitchActiveDatabaseSession(dbId1, ref databaseSessions.Map[dbId1]); else if (activeDbId == dbId2)