Skip to content

Commit 9812d0b

Browse files
ramonsmitsmauroservientidanielmarbachandreasohlund
authored
Allow JIT killing of Raven.Embedded.EmbeddedServer by tearing it down as part of stop sequence (#4824)
* Teardown `Raven.Embedded.EmbeddedServer` as part of stop sequence so that we kill the instance if needed as adhering to cancellation is not possible in host builder root container dispose. * Removed static * Add why we set `GracefulShutdownTimeout` to Zero during dispose * Enable Raven.Embedded console and file logging * Set `GracefulShutdownTimeout` to 1 hour, as default is 30 seconds. * Workaround for failing tests that seems to re-use the instance. Is that even supported? * WaitForExitAsync is needed to wait for process to exit after kill signal * Apply suggestions from Daniel's code review Co-authored-by: Daniel Marbach <[email protected]> * Ported disposing of RavenDB during stop to primary instance * Dispose Process instance `using var`, ensure EmbeddedServer.Instance.Dispose always gets invoked, added comment and improved code readability * Keep logging behavior default as it, only create client logs with LogMode.Information * Update src/ServiceControl.RavenDB/EmbeddedDatabase.cs * Update src/ServiceControl.Persistence.RavenDB/IRavenPersistenceLifecycle.cs Co-authored-by: Mauro Servienti <[email protected]> * Improved comments and cancellation behavior * Fix comment --------- Co-authored-by: Mauro Servienti <[email protected]> Co-authored-by: Daniel Marbach <[email protected]> Co-authored-by: Andreas Öhlund <[email protected]>
1 parent 5b7b35a commit 9812d0b

File tree

9 files changed

+114
-8
lines changed

9 files changed

+114
-8
lines changed

src/ServiceControl.Audit.Persistence.RavenDB/IRavenPersistenceLifecycle.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66
interface IRavenPersistenceLifecycle
77
{
88
Task Initialize(CancellationToken cancellationToken = default);
9+
Task Stop(CancellationToken cancellationToken = default);
910
}
1011
}

src/ServiceControl.Audit.Persistence.RavenDB/RavenEmbeddedPersistenceLifecycle.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ public async Task Initialize(CancellationToken cancellationToken = default)
7272
}
7373
}
7474

75+
public Task Stop(CancellationToken cancellationToken = default) => database!.Stop(cancellationToken);
76+
7577
public void Dispose()
7678
{
7779
documentStore?.Dispose();

src/ServiceControl.Audit.Persistence.RavenDB/RavenExternalPersistenceLifecycle.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ public async Task Initialize(CancellationToken cancellationToken = default)
6464
}
6565
}
6666

67+
public Task Stop(CancellationToken cancellationToken = default) => Task.CompletedTask; // We are not stopping an external instance
68+
6769
public void Dispose() => documentStore?.Dispose();
6870

6971
IDocumentStore? documentStore;

src/ServiceControl.Audit.Persistence.RavenDB/RavenPersistenceLifecycleHostedService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ sealed class RavenPersistenceLifecycleHostedService(IRavenPersistenceLifecycle l
88
{
99
public Task StartAsync(CancellationToken cancellationToken) => lifecycle.Initialize(cancellationToken);
1010

11-
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
11+
public Task StopAsync(CancellationToken cancellationToken) => lifecycle.Stop(cancellationToken);
1212
}
1313
}

src/ServiceControl.Persistence.RavenDB/IRavenPersistenceLifecycle.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ namespace ServiceControl.Persistence.RavenDB
88
interface IRavenPersistenceLifecycle
99
{
1010
Task Initialize(CancellationToken cancellationToken = default);
11+
Task Stop(CancellationToken cancellationToken = default);
1112
}
1213
}

src/ServiceControl.Persistence.RavenDB/RavenEmbeddedPersistenceLifecycle.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ public async Task Initialize(CancellationToken cancellationToken)
6969
}
7070
}
7171

72+
public async Task Stop(CancellationToken cancellationToken)
73+
{
74+
if (database != null)
75+
{
76+
await database.Stop(cancellationToken);
77+
}
78+
}
79+
7280
public void Dispose()
7381
{
7482
documentStore?.Dispose();

src/ServiceControl.Persistence.RavenDB/RavenExternalPersistenceLifecycle.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ public async Task Initialize(CancellationToken cancellationToken)
5959
}
6060
}
6161

62+
public Task Stop(CancellationToken cancellationToken) => Task.CompletedTask;
63+
6264
public void Dispose() => documentStore?.Dispose();
6365

6466
IDocumentStore? documentStore;

src/ServiceControl.Persistence.RavenDB/RavenPersistenceLifecycleHostedService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ class RavenPersistenceLifecycleHostedService(IRavenPersistenceLifecycle persiste
1010
{
1111
public Task StartAsync(CancellationToken cancellationToken) => persistenceLifecycle.Initialize(cancellationToken);
1212

13-
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
13+
public Task StopAsync(CancellationToken cancellationToken) => persistenceLifecycle.Stop(cancellationToken);
1414
}
1515
}

src/ServiceControl.RavenDB/EmbeddedDatabase.cs

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ namespace ServiceControl.RavenDB
1515
using Raven.Client.Documents.Conventions;
1616
using Raven.Client.ServerWide.Operations;
1717
using Raven.Embedded;
18+
using Sparrow.Logging;
1819

1920
public sealed class EmbeddedDatabase : IDisposable
2021
{
@@ -52,6 +53,20 @@ public static EmbeddedDatabase Start(EmbeddedDatabaseConfiguration databaseConfi
5253

5354
var nugetPackagesPath = Path.Combine(databaseConfiguration.DbPath, "Packages", "NuGet");
5455

56+
var logMode = Enum.Parse<LogMode>(databaseConfiguration.LogsMode);
57+
58+
if (logMode == LogMode.Information) // Most verbose
59+
{
60+
LoggingSource.Instance.EnableConsoleLogging();
61+
LoggingSource.Instance.SetupLogMode(
62+
logMode,
63+
Path.Combine(databaseConfiguration.LogPath, "Raven.Embedded"),
64+
retentionTime: TimeSpan.FromDays(14),
65+
retentionSize: 1024 * 1024 * 10,
66+
compress: false
67+
);
68+
}
69+
5570
Logger.InfoFormat("Loading RavenDB license from {0}", licenseFileNameAndServerDirectory.LicenseFileName);
5671
var serverOptions = new ServerOptions
5772
{
@@ -85,6 +100,7 @@ public static EmbeddedDatabase Start(EmbeddedDatabaseConfiguration databaseConfi
85100

86101
void Start(ServerOptions serverOptions)
87102
{
103+
this.serverOptions = serverOptions;
88104
EmbeddedServer.Instance.ServerProcessExited += OnServerProcessExited;
89105
EmbeddedServer.Instance.StartServer(serverOptions);
90106

@@ -162,23 +178,95 @@ public async Task<IDocumentStore> Connect(CancellationToken cancellationToken)
162178

163179
public async Task DeleteDatabase(string dbName)
164180
{
165-
using var store = await EmbeddedServer.Instance.GetDocumentStoreAsync(new DatabaseOptions(dbName) { SkipCreatingDatabase = true });
181+
using var store = await EmbeddedServer.Instance.GetDocumentStoreAsync(new DatabaseOptions(dbName)
182+
{
183+
SkipCreatingDatabase = true
184+
});
166185
await store.Maintenance.Server.SendAsync(new DeleteDatabasesOperation(dbName, true));
167186
}
168187

188+
public async Task Stop(CancellationToken cancellationToken)
189+
{
190+
Logger.Debug("Stopping RavenDB server");
191+
EmbeddedServer.Instance.ServerProcessExited -= OnServerProcessExited;
192+
193+
await shutdownTokenSource.CancelAsync();
194+
195+
// This is a workaround until the EmbeddedServer properly supports cancellation!
196+
//
197+
// EmbeddedServer does not have an async Stop method, the Dispose operation blocks and waits
198+
// until its GracefulShutdownTimeout is reached and then does a Process.Kill. Due to this behavior this can
199+
// be shorter or longer than the allowed stop duration.
200+
//
201+
// When Task.WhenAny is called, 2 things can happen:
202+
//
203+
// a. The Task.Delay gets cancelled first
204+
// b. The EmbeddedServer.Dispose completes first
205+
//
206+
// If the Task.Delay gets cancelled first this means Dispose is still running and
207+
// then we try and kill the process, if not disposed completed and we're done.
208+
209+
serverOptions!.GracefulShutdownTimeout = TimeSpan.FromHours(1); // During Stop/Dispose we manually control this
210+
211+
212+
// Dispose always need to be invoked, even when already cancelled
213+
var disposeTask = Task.Run(() => EmbeddedServer.Instance.Dispose(), CancellationToken.None);
214+
215+
// Runs "infinite" but will eventually get cancelled
216+
var waitForCancellationTask = Task.Delay(Timeout.Infinite, cancellationToken);
217+
218+
var delayIsCancelled = waitForCancellationTask == await Task.WhenAny(disposeTask, waitForCancellationTask);
219+
220+
if (delayIsCancelled)
221+
{
222+
int processId = 0;
223+
try
224+
{
225+
// We always want to try and kill the process, even when already cancelled
226+
processId = await EmbeddedServer.Instance.GetServerProcessIdAsync(CancellationToken.None);
227+
Logger.WarnFormat("Killing RavenDB server PID {0} because host cancelled", processId);
228+
using var ravenChildProcess = Process.GetProcessById(processId);
229+
ravenChildProcess.Kill(entireProcessTree: true);
230+
// Kill only signals
231+
Logger.WarnFormat("Waiting for RavenDB server PID {0} to exit... ", processId);
232+
// When WaitForExitAsync returns, the process could still exist but in a frozen state to flush
233+
// memory mapped pages to storage.
234+
await ravenChildProcess.WaitForExitAsync(CancellationToken.None);
235+
}
236+
catch (Exception e)
237+
{
238+
Logger.ErrorFormat("Failed to kill RavenDB server PID {0} shutdown\n{1}", processId, e);
239+
}
240+
}
241+
242+
serverOptions = null!;
243+
Logger.Debug("Stopped RavenDB server");
244+
}
245+
169246
public void Dispose()
170247
{
171248
if (disposed)
172249
{
173250
return;
174251
}
175252

176-
EmbeddedServer.Instance.ServerProcessExited -= OnServerProcessExited;
253+
if (serverOptions != null)
254+
{
255+
EmbeddedServer.Instance.ServerProcessExited -= OnServerProcessExited;
256+
}
177257

178258
shutdownTokenSource.Cancel();
179-
Logger.Debug("Disposing RavenDB server");
180-
EmbeddedServer.Instance.Dispose();
181-
Logger.Debug("Dispose RavenDB server");
259+
260+
if (serverOptions != null)
261+
{
262+
// Set GracefulShutdownTimeout to Zero and exit ASAP, under normal operation instance would already
263+
// have been allowed to gracefully stop during "Stop" method.
264+
serverOptions!.GracefulShutdownTimeout = TimeSpan.Zero;
265+
Logger.Debug("Disposing RavenDB server");
266+
EmbeddedServer.Instance.Dispose();
267+
Logger.Debug("Disposed RavenDB server");
268+
}
269+
182270
shutdownTokenSource.Dispose();
183271
applicationStoppingRegistration.Dispose();
184272

@@ -213,6 +301,7 @@ static long DataSize(EmbeddedDatabaseConfiguration configuration)
213301
{
214302
return -1;
215303
}
304+
216305
return info.Length;
217306
}
218307
catch
@@ -262,8 +351,9 @@ static long DirSize(DirectoryInfo d)
262351
readonly EmbeddedDatabaseConfiguration configuration;
263352
readonly CancellationToken shutdownCancellationToken;
264353
readonly CancellationTokenRegistration applicationStoppingRegistration;
354+
ServerOptions? serverOptions;
265355

266356
static TimeSpan delayBetweenRestarts = TimeSpan.FromSeconds(60);
267357
static readonly ILog Logger = LogManager.GetLogger<EmbeddedDatabase>();
268358
}
269-
}
359+
}

0 commit comments

Comments
 (0)