Skip to content

Commit

Permalink
Merged PR 875: add readiness/liveness probes to workflow
Browse files Browse the repository at this point in the history
add healthz file creation based on health check status

now workflow adds a readiness/liveness health check, and also a health check publisher.
This way, the health check system periodically executes this check.

  - create file when reporting healthy
  - delete file when reporting unhealthy
  - add health check
  - add health check publisher to o the service container,
  - add publisher unit tests
  - add readiness/liveness probes cfg

solved: #133010
  • Loading branch information
ferantivero committed Nov 11, 2019
1 parent 78cd88b commit 9c5dbca
Show file tree
Hide file tree
Showing 6 changed files with 288 additions and 1 deletion.
38 changes: 38 additions & 0 deletions charts/workflow/templates/workflow-deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,42 @@ spec:
- name: fabrikam-workflow
image: {{ .Values.dockerregistry }}{{ .Values.dockerregistrynamespace }}/{{ .Values.image.repository }}:{{ .Values.image.tag }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
readinessProbe:
exec:
command:
{{- range .Values.readinessProbe.exec.command }}
- {{ . | quote }}
{{- end }}
{{- if .Values.readinessProbe.initialDelaySeconds }}
initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
{{- end }}
{{- if .Values.readinessProbe.periodSeconds }}
periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
{{- end }}
{{- if .Values.readinessProbe.timeoutSeconds }}
timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }}
{{- end }}
{{- if .Values.readinessProbe.failureThreshold }}
failureThreshold: {{ .Values.readinessProbe.failureThreshold }}
{{- end }}
livenessProbe:
exec:
command:
{{- range .Values.livenessProbe.exec.command }}
- {{ . | quote }}
{{- end }}
{{- if .Values.livenessProbe.initialDelaySeconds }}
initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
{{- end }}
{{- if .Values.livenessProbe.periodSeconds }}
periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
{{- end }}
{{- if .Values.livenessProbe.timeoutSeconds }}
timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }}
{{- end }}
{{- if .Values.livenessProbe.failureThreshold }}
failureThreshold: {{ .Values.livenessProbe.failureThreshold }}
{{- end }}
resources:
requests:
cpu: {{ required "A valid .Values.resources.requests.cpu entry required!" .Values.resources.requests.cpu }}
Expand All @@ -60,6 +96,8 @@ spec:
env:
- name: CONFIGURATION_FOLDER
value: /kvmnt
- name: HEALTHCHECK_INITIAL_DELAY
value: {{ default "30000" .Values.healthcheck.delay | quote }}
- name: SERVICE_URI_DELIVERY
value: {{ .Values.serviceuri.delivery }}
- name: SERVICE_URI_DRONE
Expand Down
22 changes: 21 additions & 1 deletion charts/workflow/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,26 @@ servicerequest:
circuitbreakerbreakduration: 30
maxbulkheadsize: 100
maxbulkheadqueuesize: 25
healthcheck:
delay:
readinessProbe:
exec:
command:
- cat
- /app/healthz
initialDelaySeconds: 40
periodSeconds: 15
timeoutSeconds: 2
failureThreshold: 5
livenessProbe:
exec:
command:
- find
- /app/healthz
- -mmin
- -1
initialDelaySeconds: 50
periodSeconds: 30
keyvault:
name:
resourcegroup:
Expand All @@ -34,4 +54,4 @@ tags:
dev: false
prod: false
qa: false
staging: false
staging: false
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
// ------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using Fabrikam.Workflow.Service.Services;
using Moq;
using Xunit;

namespace Fabrikam.Workflow.Service.Tests
{
public class ReadinessLivenessPublisherTests
{
private const int DelayCompletionMs = 1000;

private readonly ReadinessLivenessPublisher _publisher;

public ReadinessLivenessPublisherTests()
{
var servicesBuilder = new ServiceCollection();
servicesBuilder.AddLogging(logging => logging.AddDebug());
var services = servicesBuilder.BuildServiceProvider();

_publisher =
new ReadinessLivenessPublisher(
services.GetService<ILogger<ReadinessLivenessPublisher>>());
}

[Fact]
public async Task WhenPublishingAndReportIsHealthy_FileExists()
{
// Arrange
var healthReportEntries = new Dictionary<string, HealthReportEntry>()
{
{"healthy", new HealthReportEntry(HealthStatus.Healthy, null,TimeSpan.MinValue, null, null) }
};

// Act
await _publisher.PublishAsync(
new HealthReport(healthReportEntries, TimeSpan.MinValue),
new CancellationTokenSource().Token);

// Arrange
Assert.True(File.Exists(ReadinessLivenessPublisher.FilePath));
}

[Fact]
public async Task WhenPublishingAndReportIsUnhealthy_FileDateTimeIsNotModified()
{
// Arrange
var healthReportEntries = new Dictionary<string, HealthReportEntry>()
{
{"healthy", new HealthReportEntry(HealthStatus.Healthy, null,TimeSpan.MinValue, null, null) }
};

await _publisher.PublishAsync(
new HealthReport(
healthReportEntries,
TimeSpan.MinValue),
new CancellationTokenSource().Token);

healthReportEntries.Add(
"unhealthy",
new HealthReportEntry(
HealthStatus.Unhealthy,
null,TimeSpan.MinValue, null, null));

// Act
DateTime healthyWriteTime = File.GetLastWriteTime(ReadinessLivenessPublisher.FilePath);
await _publisher.PublishAsync(
new HealthReport(healthReportEntries, TimeSpan.MinValue),
new CancellationTokenSource().Token);

// Arrange
Assert.True(File.Exists(ReadinessLivenessPublisher.FilePath));
Assert.Equal(healthyWriteTime, File.GetLastWriteTime(ReadinessLivenessPublisher.FilePath));
}

[Fact(Timeout = DelayCompletionMs * 3)]
public async Task WhenPublishingAndReportIsHealthyTwice_FileDateTimeIsModified()
{
// Arrange
Func<Task> emulatePeriodicHealthCheckAsync =
() => Task.Delay(DelayCompletionMs);

var healthReportEntries = new Dictionary<string, HealthReportEntry>()
{
{"healthy", new HealthReportEntry(HealthStatus.Healthy, null,TimeSpan.MinValue, null, null) }
};

// Act
await _publisher.PublishAsync(
new HealthReport(
healthReportEntries,
TimeSpan.MinValue),
new CancellationTokenSource().Token);

DateTime firstTimehealthyWriteTime = File.GetLastWriteTime(ReadinessLivenessPublisher.FilePath);

await emulatePeriodicHealthCheckAsync();

await _publisher.PublishAsync(
new HealthReport(healthReportEntries, TimeSpan.MinValue),
new CancellationTokenSource().Token);

DateTime sencondTimehealthyWriteTime = File.GetLastWriteTime(ReadinessLivenessPublisher.FilePath);

// Arrange
Assert.True(firstTimehealthyWriteTime < sencondTimehealthyWriteTime);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.6.1" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.ApplicationInsights" Version="2.9.1" />
<PackageReference Include="Microsoft.ApplicationInsights.Kubernetes" Version="1.0.2" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
Expand Down
27 changes: 27 additions & 0 deletions src/shipping/workflow/Fabrikam.Workflow.Service/ServiceStartup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using Fabrikam.Workflow.Service.RequestProcessing;
using Fabrikam.Workflow.Service.Services;
Expand All @@ -13,6 +15,9 @@ namespace Fabrikam.Workflow.Service
{
public static class ServiceStartup
{
private const string HealthCheckName = "ReadinessLiveness";
private const string HealthCheckServiceAssembly = "Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckPublisherHostedService";

public static void ConfigureServices(HostBuilderContext context, IServiceCollection services)
{
services.AddOptions();
Expand All @@ -26,6 +31,20 @@ public static void ConfigureServices(HostBuilderContext context, IServiceCollect

services.AddTransient<IRequestProcessor, RequestProcessor>();

// Add health check │
services.AddHealthChecks().AddCheck(
HealthCheckName,
() => HealthCheckResult.Healthy("OK"));

if (context.Configuration["HEALTHCHECK_INITIAL_DELAY"] is var configuredDelay &&
double.TryParse(configuredDelay, out double delay))
{
services.Configure<HealthCheckPublisherOptions>(options =>
{
options.Delay = TimeSpan.FromMilliseconds(delay);
});
}

services
.AddHttpClient<IPackageServiceCaller, PackageServiceCaller>(c =>
{
Expand All @@ -46,6 +65,14 @@ public static void ConfigureServices(HostBuilderContext context, IServiceCollect
c.BaseAddress = new Uri(context.Configuration["SERVICE_URI_DELIVERY"]);
})
.AddResiliencyPolicies(context.Configuration);

// workaround .NET Core 2.2: for more info https://github.com/aspnet/AspNetCore.Docs/blob/master/aspnetcore/host-and-deploy/health-checks/samples/2.x/HealthChecksSample/LivenessProbeStartup.cs#L51
services.TryAddEnumerable(
ServiceDescriptor.Singleton(typeof(IHostedService),
typeof(HealthCheckPublisherOptions).Assembly
.GetType(HealthCheckServiceAssembly)));

services.AddSingleton<IHealthCheckPublisher, ReadinessLivenessPublisher>();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
// ------------------------------------------------------------

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;

namespace Fabrikam.Workflow.Service.Services
{
public class ReadinessLivenessPublisher : IHealthCheckPublisher
{
public const string FilePath = "healthz";

private readonly ILogger _logger;

public ReadinessLivenessPublisher(ILogger<ReadinessLivenessPublisher> logger)
{
this._logger = logger;
}

public Task PublishAsync(HealthReport report,
CancellationToken cancellationToken)
{
switch (report.Status)
{
case HealthStatus.Healthy:
{
this._logger.LogInformation(
"{Timestamp} Readiness/Liveness Probe Status: {Result}",
DateTime.UtcNow,
report.Status);

CreateOrUpdateHealthz();

break;
}

case HealthStatus.Degraded:
{
this._logger.LogWarning(
"{Timestamp} Readiness/Liveness Probe Status: {Result}",
DateTime.UtcNow,
report.Status);

break;
}

case HealthStatus.Unhealthy:
{
this._logger.LogError(
"{Timestamp} Readiness Probe/Liveness Status: {Result}",
DateTime.UtcNow,
report.Status);

break;
}
}

cancellationToken.ThrowIfCancellationRequested();

return Task.CompletedTask;
}

private static void CreateOrUpdateHealthz()
{
if (File.Exists(FilePath))
{
File.SetLastWriteTimeUtc(FilePath, DateTime.UtcNow);
}
else
{
File.AppendText(FilePath).Close();
}
}
}
}

0 comments on commit 9c5dbca

Please sign in to comment.