Alexcei Shmakov
Alexcei Shmakov

Reputation: 2353

Caching Health Check Net Core

Net Core 2.2 has a support to execute health checks for published services. I would like to cache a response of checking.

I see that I can use the HealthCheckOptions and set true value for the AllowCachingResponses property.

app.UseHealthChecks("/api/services/healthCheck",
    new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions()
    {
        AllowCachingResponses = true
    });

But I don't understand how I can set the amount time caching. What is the best place to set corresponding HTTP headers(Cache-Control, Expires, etc.) and how?

My service is published by IIS.

Upvotes: 2

Views: 3572

Answers (2)

nullPainter
nullPainter

Reputation: 3056

I have a scheduled job inside my APIs which invokes HealthCheckService.CheckHealthAsync() and stores the HealthReport result. I then just create regular API endpoints which returns this value. Far simpler and doesn't require an artificial wrapper health check.

Edit: As requested, hopefully enough supporting code to demonstrate.

Controller method:

    /// <summary>
    ///     Returns the last global health check result.
    /// </summary>
    [AllowAnonymous]
    [Route("api/health"), HttpGet]
    public HealthReport Health() => HealthJob.LastReport;

And bits of the scheduled job. You will need to register the BackgroundService using AddHostedService<HealthJob>() on IServiceCollection:

public class HealthJob : BackgroundService
{
    private static readonly TimeSpan CheckInterval = TimeSpan.FromSeconds(10);

    // [snip]

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                LastReport = await _healthCheckService.CheckHealthAsync(stoppingToken);
                LastExecution = DateTime.Now;
            }
            catch (TaskCanceledException)
            {
                // Ignored
            }
            catch (Exception e)
            {
                // [snip]
                //
                // TODO assign LastReport to error values
            
            }
            
            await Task.Delay(CheckInterval, stoppingToken);            
        }
    }
}

Upvotes: 1

Tobias J
Tobias J

Reputation: 22883

The AllowCachingResponses option you mention relates only to whether HTTP headers are set by the HealthCheckMiddleware. Typically, intermediate servers, proxies, etc. may cache the result of a GET request and those headers indicate that the server should re-fetch them every time.

However, if your load balancer is using these checks to indicate whether the service should receive more traffic, it is likely not caching the results anyway.

To accomplish what you're looking for, you would need to write additional logic. One approach would be to write a type of HealthCheckCacher like the following:

public class HealthCheckCacher : IHealthCheck
{
    private readonly SemaphoreSlim _mutex = new SemaphoreSlim(1);
    private readonly IHealthCheck _healthCheck;
    private readonly TimeSpan _timeToLive;

    private HealthCheckResult _result;
    private DateTime _lastCheck;

    public static readonly TimeSpan DefaultTimeToLive = TimeSpan.FromSeconds(30);

    /// <summary>
    /// Creates a new HealthCheckCacher which will cache the result for the amount of time specified.
    /// </summary>
    /// <param name="healthCheck">The underlying health check to perform.</param>
    /// <param name="timeToLive">The amount of time for which the health check should be cached. Defaults to 30 seconds.</param>
    public HealthCheckCacher(IHealthCheck healthCheck, TimeSpan? timeToLive = null)
    {
        _healthCheck = healthCheck;
        _timeToLive = timeToLive ?? DefaultTimeToLive;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        // you could improve thread concurrency by separating the read/write logic but this would require additional thread safety checks.
        // will throw OperationCanceledException if the token is canceled while we're waiting.
        await _mutex.WaitAsync(cancellationToken);

        try
        {
            // previous check is cached & not yet expired; just return it
            if (_lastCheck > DateTime.MinValue && DateTime.Now - _lastCheck < _timeToLive)
                return _result;

            // check has not been performed or is expired; run it now & cache the result
            _result = await _healthCheck.CheckHealthAsync(context, cancellationToken);
            _lastCheck = DateTime.Now;

            return _result;
        }
        finally
        {
            _mutex.Release();
        }
    }
}

Upvotes: 1

Related Questions