Skip to content

Orleans Health Checks using ASP.NET Core

Orleans Health Checks

One of the nicest things with the introduction of the Generic Host in Orleans v3.0 is that it allows you to run Orleans side by side with ASP.NET Core. One of the main benefits that I could think of is creating Orleans health checks using ASP.NET Core Health Checks.

Co-Hosting

Since Orleans 3.0, there is now support for the GenericHost via extensions on IHostBuilder, this allows you to run multiple services within the same process all sharing things such as logging, services, configuration, etc.

In a recent blog post, I covered how you can co-host Orleans and ASP.NET Core. This post is an extension of that. If you have not read that already, go take a look first.

ASP.NET Core Health Checks

The reason for wanting health checks for services like an Orleans Silo are varied. One primary use case is when running a silo that is hosted under some type of container orchestration, such as Kubernetes, AWS ECS, etc.

These orchestrators generally provide a mechanism to determine if your service is healthy or unhealthy. If unhealthy, the will create new instances based on your configuration.

ASP.NET Core has a middleware that simplifies providing such health status. I’ve blogged about creating ASP.NET Core Health Checks over on the Telerik Blog.

Orleans Health Checks

In order to verify that a local silo is healthy, we are going to use a stateless grain. The reason being, stateless grains are always executed locally.

Requests made to Stateless Worker grains are always executed locally, that is on the same silo where the request originated, either made by a grain running on the silo or received by the silo’s client gateway. Hence, calls to Stateless Worker grains from other grains or from client gateways never incur a remote message.

using System.Threading.Tasks;
using Orleans.Concurrency;
namespace Orleans.AspNetCore.Demo.Grains
{
public interface IHealthCheckGrain : IGrainWithGuidKey
{
Task<bool> IsHealthy();
}
[StatelessWorker(1)]
public class HealthCheckGrain : Grain, IHealthCheckGrain
{
public Task<bool> IsHealthy()
{
return Task.FromResult(true);
}
}
}

Our grain isn’t really doing anything, and it doesn’t need to. I’ve made it return a bool however, it could just return a Task. Returning a bool might be useful if you plan on doing any other check within the grain, such as maybe verify connections to db, etc.

Next, we’ll create our actual ASP.NET Core Health Check by implementing the IHealthCheck.

All our implementation is going to do is get the IHealthCheckGrain. If anything fails, we’ll return Unhealthy, otherwise its all good so we’ll return Healthy.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Orleans.AspNetCore.Demo.Grains;
namespace Orleans.AspNetCore.Demo
{
public class OrleansHealthCheck : IHealthCheck
{
private readonly IClusterClient _clusterClient;
public OrleansHealthCheck(IClusterClient clusterClient)
{
_clusterClient = clusterClient;
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
if (_clusterClient.IsInitialized == false)
{
return HealthCheckResult.Healthy("Initializing");
}
try
{
return await _clusterClient.GetGrain<IHealthCheckGrain>(Guid.Empty).IsHealthy()
? HealthCheckResult.Healthy()
: HealthCheckResult.Unhealthy();
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("HealthCheckGrain failed.", ex);
}
}
}
public static class OrleansHealthCheckBuilderExtension
{
private const string HealthCheckName = "OrleansHealthCheck";
public static IHealthChecksBuilder AddOrleansHealthCheck(this IHealthChecksBuilder builder)
{
return builder.Add(new HealthCheckRegistration(HealthCheckName, sp => new OrleansHealthCheck( sp.GetRequiredService<IClusterClient>()), HealthStatus.Unhealthy, new string[0]));
}
}
}

As you can see, I also added an extension method off of IHealthChecksBuilder just to simplify registering our new health check.

Finally, to wire up our new health check, we need to add AddHealthChecks and AddOrleansHealthCheck to our ConfigureServices. As well we need to call UseHealthChecks in the Configure method.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Orleans.AspNetCore.Demo
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddHealthChecks().AddOrleansHealthCheck();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseHealthChecks("/health");
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
view raw Startup.cs hosted with ❤ by GitHub

Results

Now if you browse or make an HTTP call to the /health endpoint, you get a 200 OK with Healthy if the local silo is running.

Related Links

Learn more about Software Architecture & Design.
Join thousands of developers getting weekly updates to increase your understanding of software architecture and design concepts.