-
Notifications
You must be signed in to change notification settings - Fork 647
Expand file tree
/
Copy pathGarnetServer.cs
More file actions
712 lines (613 loc) · 30.5 KB
/
GarnetServer.cs
File metadata and controls
712 lines (613 loc) · 30.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Garnet.cluster;
using Garnet.common;
using Garnet.networking;
using Garnet.server;
using Garnet.server.Auth.Settings;
using Microsoft.Extensions.Logging;
using Tsavorite.core;
namespace Garnet
{
using MainStoreAllocator = SpanByteAllocator<StoreFunctions<SpanByte, SpanByte, SpanByteComparer, SpanByteRecordDisposer>>;
using MainStoreFunctions = StoreFunctions<SpanByte, SpanByte, SpanByteComparer, SpanByteRecordDisposer>;
using ObjectStoreAllocator = GenericAllocator<byte[], IGarnetObject, StoreFunctions<byte[], IGarnetObject, ByteArrayKeyComparer, DefaultRecordDisposer<byte[], IGarnetObject>>>;
using ObjectStoreFunctions = StoreFunctions<byte[], IGarnetObject, ByteArrayKeyComparer, DefaultRecordDisposer<byte[], IGarnetObject>>;
/// <summary>
/// Implementation Garnet server
/// </summary>
public class GarnetServer : IDisposable
{
/// <summary>
/// Resp protocol version
/// </summary>
internal const string RedisProtocolVersion = "7.4.3";
static readonly string version = GetVersion();
static string GetVersion()
{
var Version = Assembly.GetExecutingAssembly().GetName().Version;
return $"{Version.Major}.{Version.Minor}.{Version.Build}";
}
internal GarnetProvider Provider;
private readonly GarnetServerOptions opts;
private IGarnetServer[] servers;
private SubscribeBroker subscribeBroker;
private KVSettings<SpanByte, SpanByte> kvSettings;
private KVSettings<byte[], IGarnetObject> objKvSettings;
private INamedDeviceFactory logFactory;
private MemoryLogger initLogger;
private ILogger logger;
private readonly ILoggerFactory loggerFactory;
private readonly bool cleanupDir;
private bool disposeLoggerFactory;
/// <summary>
/// Store and associated information used by this Garnet server
/// </summary>
protected StoreWrapper storeWrapper;
/// <summary>
/// Metrics API
/// </summary>
public MetricsApi Metrics;
/// <summary>
/// Command registration API
/// </summary>
public RegisterApi Register;
/// <summary>
/// Store API
/// </summary>
public StoreApi Store;
/// <summary>
/// Create Garnet Server instance using specified command line arguments; use Start to start the server.
/// </summary>
/// <param name="commandLineArgs">Command line arguments</param>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="cleanupDir">Clean up directory.</param>
/// <param name="authenticationSettingsOverride">Override for custom authentication settings.</param>
public GarnetServer(string[] commandLineArgs, ILoggerFactory loggerFactory = null, bool cleanupDir = false, IAuthenticationSettings authenticationSettingsOverride = null)
{
Trace.Listeners.Add(new ConsoleTraceListener());
// Set up an initial memory logger to log messages from configuration parser into memory.
using (var memLogProvider = new MemoryLoggerProvider())
{
this.initLogger = (MemoryLogger)memLogProvider.CreateLogger("ArgParser");
}
if (!ServerSettingsManager.TryParseCommandLineArguments(commandLineArgs, out var serverSettings, out _, out _, out var exitGracefully, logger: this.initLogger))
{
if (exitGracefully)
Environment.Exit(0);
// Flush logs from memory logger
FlushMemoryLogger(this.initLogger, "ArgParser", loggerFactory);
throw new GarnetException("Encountered an error when initializing Garnet server. Please see log messages above for more details.");
}
if (loggerFactory == null)
{
// If the main logger factory is created by GarnetServer, it should be disposed when GarnetServer is disposed
disposeLoggerFactory = true;
}
else
{
this.initLogger.LogWarning(
$"Received an external ILoggerFactory object. The following configuration options are ignored: {nameof(serverSettings.FileLogger)}, {nameof(serverSettings.LogLevel)}, {nameof(serverSettings.DisableConsoleLogger)}.");
}
// If no logger factory is given, set up main logger factory based on parsed configuration values,
// otherwise use given logger factory.
this.loggerFactory = loggerFactory ?? LoggerFactory.Create(builder =>
{
if (!serverSettings.DisableConsoleLogger.GetValueOrDefault())
{
builder.AddSimpleConsole(options =>
{
options.SingleLine = true;
options.TimestampFormat = "hh::mm::ss ";
});
}
// Optional: Flush log output to file.
if (serverSettings.FileLogger != null)
builder.AddFile(serverSettings.FileLogger);
builder.SetMinimumLevel(serverSettings.LogLevel);
});
// Assign values to GarnetServerOptions
this.opts = serverSettings.GetServerOptions(this.loggerFactory.CreateLogger("Options"));
this.opts.AuthSettings = authenticationSettingsOverride ?? this.opts.AuthSettings;
this.cleanupDir = cleanupDir;
this.InitializeServer();
}
/// <summary>
/// Create Garnet Server instance using GarnetServerOptions instance; use Start to start the server.
/// </summary>
/// <param name="opts">Server options</param>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="servers">The IGarnetServer to use. If none is provided, will use a GarnetServerTcp.</param>
/// <param name="cleanupDir">Whether to clean up data folders on dispose</param>
public GarnetServer(GarnetServerOptions opts, ILoggerFactory loggerFactory = null, IGarnetServer[] servers = null, bool cleanupDir = false)
{
this.servers = servers;
this.opts = opts;
this.loggerFactory = loggerFactory;
this.cleanupDir = cleanupDir;
this.InitializeServer();
}
private void InitializeServer()
{
Debug.Assert(opts != null);
if (!opts.QuietMode)
{
var red = "\u001b[31m";
var magenta = "\u001b[35m";
var normal = "\u001b[0m";
Console.WriteLine($"""
{red} _________
/_||___||_\ {normal}Garnet {version} {(IntPtr.Size == 8 ? "64" : "32")} bit; {(opts.EnableCluster ? "cluster" : "standalone")} mode{red}
'. \ / .' {normal}Listening on: {(opts.EndPoints.Length > 1 ? opts.EndPoints[0] + $" and {opts.EndPoints.Length - 1} more" : opts.EndPoints[0])}{red}
'.\ /.' {magenta}https://aka.ms/GetGarnet{red}
'.'
{normal}
""");
}
var clusterFactory = opts.EnableCluster ? new ClusterFactory() : null;
this.logger = this.loggerFactory?.CreateLogger("GarnetServer");
logger?.LogInformation("Garnet {version} {bits} bit; {clusterMode} mode; Endpoint: [{endpoint}]",
version, IntPtr.Size == 8 ? "64" : "32",
opts.EnableCluster ? "cluster" : "standalone",
string.Join(',', opts.EndPoints.Select(endpoint => endpoint.ToString())));
logger?.LogInformation("Environment .NET {netVersion}; {osPlatform}; {processArch}", Environment.Version, Environment.OSVersion.Platform, RuntimeInformation.ProcessArchitecture);
// Flush initialization logs from memory logger
FlushMemoryLogger(this.initLogger, "ArgParser", this.loggerFactory);
var customCommandManager = new CustomCommandManager();
ThreadPool.GetMinThreads(out var minThreads, out var minCPThreads);
ThreadPool.GetMaxThreads(out var maxThreads, out var maxCPThreads);
bool minChanged = false, maxChanged = false;
if (opts.ThreadPoolMinThreads > 0)
{
minThreads = opts.ThreadPoolMinThreads;
minChanged = true;
}
if (opts.ThreadPoolMinIOCompletionThreads > 0)
{
minCPThreads = opts.ThreadPoolMinIOCompletionThreads;
minChanged = true;
}
if (opts.ThreadPoolMaxThreads > 0)
{
maxThreads = opts.ThreadPoolMaxThreads;
maxChanged = true;
}
if (opts.ThreadPoolMaxIOCompletionThreads > 0)
{
maxCPThreads = opts.ThreadPoolMaxIOCompletionThreads;
maxChanged = true;
}
// First try to set the max threads
var setMax = !maxChanged || ThreadPool.SetMaxThreads(maxThreads, maxCPThreads);
// Set the min threads
if (minChanged && !ThreadPool.SetMinThreads(minThreads, minCPThreads))
throw new Exception($"Unable to call ThreadPool.SetMinThreads with {minThreads}, {minCPThreads}");
// Retry to set max threads if it wasn't set in the earlier step
if (!setMax && !ThreadPool.SetMaxThreads(maxThreads, maxCPThreads))
throw new Exception($"Unable to call ThreadPool.SetMaxThreads with {maxThreads}, {maxCPThreads}");
opts.Initialize(loggerFactory);
StoreWrapper.DatabaseCreatorDelegate createDatabaseDelegate = (int dbId) =>
CreateDatabase(dbId, opts, clusterFactory, customCommandManager);
if (!opts.DisablePubSub)
subscribeBroker = new SubscribeBroker(null, opts.PubSubPageSizeBytes(), opts.SubscriberRefreshFrequencyMs, true, logger);
logger?.LogTrace("TLS is {tlsEnabled}", opts.TlsOptions == null ? "disabled" : "enabled");
// Create Garnet TCP server if none was provided.
if (servers == null)
{
servers = new IGarnetServer[opts.EndPoints.Length];
for (var i = 0; i < servers.Length; i++)
{
if (opts.EndPoints[i] is UnixDomainSocketEndPoint)
{
ArgumentException.ThrowIfNullOrWhiteSpace(opts.UnixSocketPath, nameof(opts.UnixSocketPath));
// Delete existing unix socket file, if it exists.
File.Delete(opts.UnixSocketPath);
}
servers[i] = new GarnetServerTcp(opts.EndPoints[i], 0, opts.TlsOptions, opts.NetworkSendThrottleMax, opts.NetworkConnectionLimit, opts.UnixSocketPath, opts.UnixSocketPermission, logger);
}
}
storeWrapper = new StoreWrapper(version, RedisProtocolVersion, servers, customCommandManager, opts, subscribeBroker,
createDatabaseDelegate: createDatabaseDelegate,
clusterFactory: clusterFactory,
loggerFactory: loggerFactory);
if (logger != null)
{
var configMemoryLimit = (storeWrapper.store.IndexSize * 64) +
storeWrapper.store.Log.MaxMemorySizeBytes +
(storeWrapper.store.ReadCache?.MaxMemorySizeBytes ?? 0) +
(storeWrapper.appendOnlyFile?.MaxMemorySizeBytes ?? 0);
if (storeWrapper.objectStore != null)
configMemoryLimit += (storeWrapper.objectStore.IndexSize * 64) +
storeWrapper.objectStore.Log.MaxMemorySizeBytes +
(storeWrapper.objectStore.ReadCache?.MaxMemorySizeBytes ?? 0) +
(storeWrapper.objectStoreSizeTracker?.TargetSize ?? 0) +
(storeWrapper.objectStoreSizeTracker?.ReadCacheTargetSize ?? 0);
logger.LogInformation("Total configured memory limit: {configMemoryLimit}", configMemoryLimit);
}
var maxDatabases = opts.EnableCluster ? 1 : opts.MaxDatabases;
logger?.LogInformation("Max number of logical databases allowed on server: {maxDatabases}", maxDatabases);
if (opts.ExtensionBinPaths?.Length > 0)
{
logger?.LogTrace("Allowed binary paths for extension loading: {binPaths}", string.Join(",", opts.ExtensionBinPaths));
logger?.LogTrace("Unsigned extension libraries {unsignedAllowed}allowed.", opts.ExtensionAllowUnsignedAssemblies ? string.Empty : "not ");
}
// Create session provider for Garnet
Provider = new GarnetProvider(storeWrapper, subscribeBroker);
// Create user facing API endpoints
Metrics = new MetricsApi(Provider);
Register = new RegisterApi(Provider);
Store = new StoreApi(storeWrapper);
for (var i = 0; i < servers.Length; i++)
servers[i].Register(WireFormat.ASCII, Provider);
LoadModules(customCommandManager);
}
private GarnetDatabase CreateDatabase(int dbId, GarnetServerOptions serverOptions, ClusterFactory clusterFactory,
CustomCommandManager customCommandManager)
{
var store = CreateMainStore(dbId, clusterFactory, out var epoch, out var stateMachineDriver);
var objectStore = CreateObjectStore(dbId, clusterFactory, customCommandManager, epoch, stateMachineDriver, out var objectStoreSizeTracker);
var (aofDevice, aof) = CreateAOF(dbId);
var vectorManager = new VectorManager(
serverOptions.EnableVectorSetPreview,
dbId,
() => Provider.GetSession(WireFormat.ASCII, null),
loggerFactory
);
return new GarnetDatabase(dbId, store, objectStore, epoch, stateMachineDriver, objectStoreSizeTracker,
aofDevice, aof, serverOptions.AdjustedIndexMaxCacheLines == 0,
serverOptions.AdjustedObjectStoreIndexMaxCacheLines == 0,
vectorManager);
}
private void LoadModules(CustomCommandManager customCommandManager)
{
if (opts.LoadModuleCS == null)
return;
foreach (var moduleCS in opts.LoadModuleCS)
{
var moduleCSData = moduleCS.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (moduleCSData.Length < 1)
continue;
var modulePath = moduleCSData[0];
var moduleArgs = moduleCSData.Length > 1 ? moduleCSData.Skip(1).ToArray() : [];
if (!ModuleUtils.LoadAssemblies([modulePath], null, opts.ExtensionAllowUnsignedAssemblies,
out var loadedAssemblies, out var errorMsg, ignorePathCheckWhenUndefined: true)
|| !ModuleRegistrar.Instance.LoadModule(customCommandManager, loadedAssemblies.ToList()[0], moduleArgs, logger, out errorMsg))
{
logger?.LogError("Module {0} failed to load with error {1}", modulePath, Encoding.UTF8.GetString(errorMsg));
}
}
}
private TsavoriteKV<SpanByte, SpanByte, MainStoreFunctions, MainStoreAllocator> CreateMainStore(int dbId, IClusterFactory clusterFactory,
out LightEpoch epoch, out StateMachineDriver stateMachineDriver)
{
epoch = new LightEpoch();
stateMachineDriver = new StateMachineDriver(epoch, loggerFactory?.CreateLogger($"StateMachineDriver"));
kvSettings = opts.GetSettings(loggerFactory, epoch, stateMachineDriver, out logFactory);
// Run checkpoint on its own thread to control p99
kvSettings.ThrottleCheckpointFlushDelayMs = opts.CheckpointThrottleFlushDelayMs;
var baseName = opts.GetMainStoreCheckpointDirectory(dbId);
var defaultNamingScheme = new DefaultCheckpointNamingScheme(baseName);
kvSettings.CheckpointManager = opts.EnableCluster ?
clusterFactory.CreateCheckpointManager(opts.DeviceFactoryCreator, defaultNamingScheme, isMainStore: true, logger) :
new GarnetCheckpointManager(opts.DeviceFactoryCreator, defaultNamingScheme, removeOutdated: true);
return new TsavoriteKV<SpanByte, SpanByte, MainStoreFunctions, MainStoreAllocator>(kvSettings
, StoreFunctions<SpanByte, SpanByte>.Create()
, (allocatorSettings, storeFunctions) => new(allocatorSettings, storeFunctions));
}
private TsavoriteKV<byte[], IGarnetObject, ObjectStoreFunctions, ObjectStoreAllocator> CreateObjectStore(int dbId, IClusterFactory clusterFactory, CustomCommandManager customCommandManager,
LightEpoch epoch, StateMachineDriver stateMachineDriver, out CacheSizeTracker objectStoreSizeTracker)
{
objectStoreSizeTracker = null;
if (opts.DisableObjects)
return null;
objKvSettings = opts.GetObjectStoreSettings(loggerFactory, epoch, stateMachineDriver,
out var objHeapMemorySize, out var objReadCacheHeapMemorySize);
// Run checkpoint on its own thread to control p99
objKvSettings.ThrottleCheckpointFlushDelayMs = opts.CheckpointThrottleFlushDelayMs;
var baseName = opts.GetObjectStoreCheckpointDirectory(dbId);
var defaultNamingScheme = new DefaultCheckpointNamingScheme(baseName);
objKvSettings.CheckpointManager = opts.EnableCluster ?
clusterFactory.CreateCheckpointManager(opts.DeviceFactoryCreator, defaultNamingScheme, isMainStore: false, logger) :
new GarnetCheckpointManager(opts.DeviceFactoryCreator, defaultNamingScheme, removeOutdated: true);
var objStore = new TsavoriteKV<byte[], IGarnetObject, ObjectStoreFunctions, ObjectStoreAllocator>(
objKvSettings,
StoreFunctions<byte[], IGarnetObject>.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 (IDevice, TsavoriteLog) CreateAOF(int dbId)
{
if (!opts.EnableAOF)
{
if (opts.CommitFrequencyMs != 0 || opts.WaitForCommit)
throw new Exception("Cannot use CommitFrequencyMs or CommitWait without EnableAOF");
return (null, null);
}
if (opts.FastAofTruncate && opts.CommitFrequencyMs != -1)
throw new Exception("Need to set CommitFrequencyMs to -1 (manual commits) with FastAofTruncate");
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 (aofDevice, appendOnlyFile);
}
/// <summary>
/// Start server instance
/// </summary>
public void Start()
{
Provider.Recover();
for (var i = 0; i < servers.Length; i++)
servers[i].Start();
Provider.Start();
if (!opts.QuietMode)
Console.WriteLine("* Ready to accept connections");
}
/// <summary>
/// Performs graceful shutdown of the server.
/// Stops accepting new connections, waits for active connections to complete, commits AOF, and takes checkpoint if needed.
/// </summary>
/// <param name="timeout">Timeout for waiting on active connections (default: 30 seconds)</param>
/// <param name="token">Cancellation token</param>
/// <returns>Task representing the async shutdown operation</returns>
public async Task ShutdownAsync(TimeSpan? timeout = null, CancellationToken token = default)
{
var shutdownTimeout = timeout ?? TimeSpan.FromSeconds(30);
try
{
// Stop accepting new connections first
StopListening();
// Wait for existing connections to complete
await WaitForActiveConnectionsAsync(shutdownTimeout, token).ConfigureAwait(false);
// Commit AOF and take checkpoint if needed
await FinalizeDataAsync(token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Force shutdown requested
}
catch (Exception ex)
{
logger?.LogError(ex, "Error during graceful shutdown");
}
}
/// <summary>
/// Stop all servers from accepting new connections.
/// </summary>
private void StopListening()
{
if (servers == null) return;
logger?.LogDebug("Stopping listeners to prevent new connections...");
foreach (var server in servers)
{
try
{
server?.StopListening();
}
catch (Exception ex)
{
logger?.LogWarning(ex, "Error stopping listener");
}
}
}
/// <summary>
/// Waits for active connections to complete within the specified timeout.
/// </summary>
private async Task WaitForActiveConnectionsAsync(TimeSpan timeout, CancellationToken token)
{
if (servers == null) return;
// Linked Token : between external token and timeout
using var cts = CancellationTokenSource.CreateLinkedTokenSource(token);
cts.CancelAfter(timeout);
var delays = new[] { 50, 300, 1000 };
var delayIndex = 0;
try
{
while (!cts.Token.IsCancellationRequested)
{
var activeConnections = GetActiveConnectionCount();
if (activeConnections == 0)
{
logger?.LogInformation("All connections have been closed gracefully.");
return;
}
logger?.LogInformation("Waiting for {ActiveConnections} active connections to complete...", activeConnections);
var currentDelay = delays[delayIndex];
if (delayIndex < delays.Length - 1) delayIndex++;
await Task.Delay(currentDelay, cts.Token).ConfigureAwait(false);
}
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
throw;
}
catch (OperationCanceledException)
{
// timeout reached error logging
logger?.LogWarning("Timeout reached after {TimeoutSeconds} seconds. Some connections may still be active.",
timeout.TotalSeconds);
}
catch (Exception ex)
{
logger?.LogWarning(ex, "Error checking active connections");
delayIndex = 0;
await Task.Delay(500, token).ConfigureAwait(false);
}
}
/// <summary>
/// Gets the current number of active connections directly from server instances.
/// </summary>
private long GetActiveConnectionCount()
{
long count = 0;
if (servers != null)
{
foreach (var garnetServer in servers)
{
if (garnetServer is GarnetServerBase garnetServerBase)
{
count += garnetServerBase.get_conn_active();
}
}
}
return count;
}
/// <summary>
/// Commits AOF and takes checkpoint for data durability during shutdown.
/// </summary>
private async Task FinalizeDataAsync(CancellationToken token)
{
// Commit AOF before checkpoint/shutdown
if (opts.EnableAOF)
{
logger?.LogDebug("Committing AOF before shutdown...");
try
{
var commitSuccess = await Store.CommitAOFAsync(token).ConfigureAwait(false);
if (commitSuccess)
{
logger?.LogDebug("AOF committed successfully.");
}
else
{
logger?.LogInformation("AOF commit skipped (another commit in progress or replica mode).");
}
}
catch (Exception ex)
{
logger?.LogError(ex, "Error committing AOF during shutdown");
}
}
// Take checkpoint for tiered storage
if (opts.EnableStorageTier)
{
logger?.LogDebug("Taking checkpoint for tiered storage...");
try
{
var checkpointSuccess = Store.TakeCheckpoint(background: false, token);
if (checkpointSuccess)
{
logger?.LogDebug("Checkpoint completed successfully.");
}
else
{
logger?.LogInformation("Checkpoint skipped (another checkpoint in progress or replica mode).");
}
}
catch (Exception ex)
{
logger?.LogError(ex, "Error taking checkpoint during shutdown");
}
}
}
/// <summary>
/// Dispose store (including log and checkpoint directory)
/// </summary>
public void Dispose()
{
Dispose(cleanupDir);
}
/// <summary>
/// Dispose, optionally deleting logs and checkpoints
/// </summary>
/// <param name="deleteDir">Whether to delete logs and checkpoints</param>
public void Dispose(bool deleteDir = true)
{
InternalDispose();
if (deleteDir)
{
logFactory?.Delete(new FileDescriptor { directoryName = "" });
if (opts.CheckpointDir != opts.LogDir && !string.IsNullOrEmpty(opts.CheckpointDir))
{
var checkpointDeviceFactory = opts.DeviceFactoryCreator.Create(opts.CheckpointDir);
checkpointDeviceFactory.Delete(new FileDescriptor { directoryName = "" });
}
}
}
private void InternalDispose()
{
Provider?.Dispose();
for (var i = 0; i < servers.Length; i++)
servers[i]?.Dispose();
subscribeBroker?.Dispose();
kvSettings.LogDevice?.Dispose();
if (!opts.DisableObjects)
{
objKvSettings.LogDevice?.Dispose();
objKvSettings.ObjectLogDevice?.Dispose();
}
opts.AuthSettings?.Dispose();
if (disposeLoggerFactory)
loggerFactory?.Dispose();
}
private static void DeleteDirectory(string path)
{
if (path == null) return;
// Exceptions may happen due to a handle briefly remaining held after Dispose().
try
{
foreach (string directory in Directory.GetDirectories(path))
{
DeleteDirectory(directory);
}
Directory.Delete(path, true);
}
catch (Exception ex) when (ex is IOException ||
ex is UnauthorizedAccessException)
{
try
{
Directory.Delete(path, true);
}
catch { }
}
}
/// <summary>
/// Flushes MemoryLogger entries into a destination logger.
/// Destination logger is either created from ILoggerFactory parameter or from a default console logger.
/// </summary>
/// <param name="memoryLogger">The memory logger</param>
/// <param name="categoryName">The category name of the destination logger</param>
/// <param name="dstLoggerFactory">Optional logger factory for creating the destination logger</param>
private static void FlushMemoryLogger(MemoryLogger memoryLogger, string categoryName, ILoggerFactory dstLoggerFactory = null)
{
if (memoryLogger == null) return;
// If no logger factory supplied, create a default console logger
var disposeDstLoggerFactory = false;
if (dstLoggerFactory == null)
{
dstLoggerFactory = LoggerFactory.Create(builder => builder.AddSimpleConsole(options =>
{
options.SingleLine = true;
options.TimestampFormat = "hh::mm::ss ";
}).SetMinimumLevel(LogLevel.Information));
disposeDstLoggerFactory = true;
}
// Create the destination logger
var dstLogger = dstLoggerFactory.CreateLogger(categoryName);
// Flush all entries from the memory logger into the destination logger
memoryLogger.FlushLogger(dstLogger);
// If a default console logger factory was created, it is no longer needed
if (disposeDstLoggerFactory)
{
dstLoggerFactory.Dispose();
}
}
}
}