Skip to content

Commit

Permalink
Delay Sidecar Startup until Host Application Started (#37)
Browse files Browse the repository at this point in the history
* Delay sidecar startup until IHostApplicationLifetime.ApplicationStarted is signaled
  • Loading branch information
badgeratu authored Jan 14, 2022
1 parent 0fb8a6d commit 6d0d616
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using Man.Dapr.Sidekick.AspNetCore.Metrics;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Man.Dapr.Sidekick.AspNetCore.Sidecar
Expand All @@ -23,6 +25,14 @@ public DaprSidecarHostedService(

protected override void OnStarting(DaprOptions options, CancellationToken cancellationToken)
{
WaitForApplicationStart(cancellationToken);

// If cancelled then exit
if (cancellationToken.IsCancellationRequested)
{
return;
}

// Assign metrics
options.Sidecar ??= new DaprSidecarOptions();
options.Sidecar.Metrics ??= new DaprMetricsOptions();
Expand Down Expand Up @@ -80,5 +90,51 @@ protected override void OnStarting(DaprOptions options, CancellationToken cancel
}
}
}

protected void WaitForApplicationStart(CancellationToken cancellationToken)
{
var logger = _serviceProvider?.GetService<ILogger<DaprSidecarHostedService>>();
#if NETCOREAPP3_0_OR_GREATER
var applicationLifetime = _serviceProvider?.GetService<IHostApplicationLifetime>();
#else
var applicationLifetime = _serviceProvider?.GetService<IApplicationLifetime>();
#endif
if (applicationLifetime == null)
{
// Not yet started, log out a waiting message
logger?.LogInformation("Host application lifetime tracking not available, starting Dapr process");

// Either no lifetime tracker, or no service provider so cannot wait for startup
return;
}

// Trigger the wait handle when the application has started to release Dapr startup process.
var waitForApplicationStart = new ManualResetEventSlim();
applicationLifetime.ApplicationStarted.Register(() => waitForApplicationStart.Set());
if (applicationLifetime.ApplicationStarted.IsCancellationRequested)
{
logger?.LogInformation("Host application ready, starting Dapr process");

// Started token has already triggered. No need to wait.
return;
}

// Ensure the host has started
while (!cancellationToken.IsCancellationRequested)
{
// Not yet started, log out a waiting message
logger?.LogInformation("Host application is initializing, waiting to start Dapr process...");

// Wait for the host to start so all configurations, environment variables
// and ports are fully initialized.
if (waitForApplicationStart.Wait(TimeSpan.FromSeconds(1), cancellationToken))
{
break;
}
}

// Not yet started, log out a waiting message
logger?.LogInformation("Host application ready, starting Dapr process");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Man.Dapr.Sidekick.Threading;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using NSubstitute;
using NUnit.Framework;
Expand Down Expand Up @@ -238,6 +239,7 @@ public async Task Should_not_wait_for_hosting_if_cancelled()

// Start a timer to cancel the process
var timer = new System.Timers.Timer(20);
timer.AutoReset = false;
timer.Elapsed += (sender, args) =>
{
cts.Cancel();
Expand All @@ -251,5 +253,51 @@ public async Task Should_not_wait_for_hosting_if_cancelled()
Assert.That(options.Sidecar.AppPort, Is.Null);
}
}

[Test]
public async Task Should_wait_for_hosting()
{
var options = new DaprOptions
{
Sidecar = new DaprSidecarOptions
{
AppId = "TEST_APP"
}
};

var optionsAccessor = Substitute.For<IOptionsMonitor<DaprOptions>>();
optionsAccessor.CurrentValue.Returns(options);
var host = Substitute.For<IDaprSidecarHost>();
var ctsApplication = new CancellationTokenSource();
var applicationLifetime = Substitute.For<IHostApplicationLifetime>();
applicationLifetime.ApplicationStarted.Returns(ctsApplication.Token);
var serviceProvider = Substitute.For<IServiceProvider>();
serviceProvider.GetService(typeof(IHostApplicationLifetime)).Returns(applicationLifetime);
var hostedService = new DaprSidecarHostedService(host, optionsAccessor, serviceProvider);
var cts = new CancellationTokenSource();

host.When(x => x.Start(Arg.Any<Func<DaprOptions>>(), Arg.Any<DaprCancellationToken>())).Do(ci =>
{
// Execute the accessor
var accessor = (Func<DaprOptions>)ci[0];
var newOptions = accessor();
});

// Start a timer to trigger the process
var timer = new System.Timers.Timer(20);
timer.AutoReset = false;
timer.Elapsed += (sender, args) =>
{
// Signal completed hosting startup
ctsApplication.Cancel();
};

timer.Start();
await hostedService.StartAsync(cts.Token);

// Sidecar should not be null (so accessor was invoked) and AppPort not set
Assert.That(options.Sidecar, Is.Not.Null);
Assert.That(options.Sidecar.AppPort, Is.Null);
}
}
}

0 comments on commit 6d0d616

Please sign in to comment.