Skip to content

Commit

Permalink
Implement disposable pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
sungam3r committed Jul 29, 2023
1 parent eaa25c2 commit cc44a46
Show file tree
Hide file tree
Showing 78 changed files with 333 additions and 246 deletions.
7 changes: 6 additions & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<PackageTags>HealthCheck;HealthChecks;Health</PackageTags>
<NoWarn>$(NoWarn);1591</NoWarn> <!--TODO: temporary solution-->
<NoWarn>$(NoWarn);1591;IDISP013</NoWarn> <!--TODO: temporary solution-->
</PropertyGroup>

<PropertyGroup Condition="$(MSBuildProjectName.EndsWith('.Tests'))">
Expand Down Expand Up @@ -76,6 +76,11 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!--Temporary disabled-->
<!--<PackageReference Include="IDisposableAnalyzers" Version="4.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>-->
</ItemGroup>

</Project>
20 changes: 20 additions & 0 deletions src/DisposalHostedService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Microsoft.Extensions.Hosting;

internal sealed class DisposalHostedService : IHostedService
{
private readonly IDisposable _disposable;

public DisposalHostedService(IDisposable disposable)
{
_disposable = disposable;
}

public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;

[System.Diagnostics.CodeAnalysis.SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP007:Don't dispose injected", Justification = "by design")]
public Task StopAsync(CancellationToken cancellationToken)
{
_disposable.Dispose();
return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, Canc
return Task.FromResult(IsApplicationRunning ? HealthCheckResult.Healthy() : HealthCheckResult.Unhealthy());
}

public void Dispose()
public virtual void Dispose()
{
_ctRegistration.Dispose();
_ctRegistration = default;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ private async Task<bool> ApplicationInsightsResourceExistsAsync(CancellationToke
try
{
var uri = new Uri(_appInsightsUrls[index++] + path);
HttpResponseMessage response = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
using var response = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,25 @@

namespace HealthChecks.AzureDigitalTwin;

public class AzureDigitalTwinSubscriptionHealthCheck
: AzureDigitalTwinHealthCheck, IHealthCheck
public class AzureDigitalTwinSubscriptionHealthCheck : AzureDigitalTwinHealthCheck, IHealthCheck
{
public AzureDigitalTwinSubscriptionHealthCheck(string clientId, string clientSecret, string tenantId)
: base(clientId, clientSecret, tenantId)
{ }
{
}

public AzureDigitalTwinSubscriptionHealthCheck(ServiceClientCredentials serviceClientCredentials)
: base(serviceClientCredentials)
{ }
{
}

/// <inheritdoc />
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var managementClient = ManagementClientConnections.GetOrAdd(ClientConnectionKey, _ => CreateManagementClient());
_ = await managementClient.Operations.ListWithHttpMessagesAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
using var _ = await managementClient.Operations.ListWithHttpMessagesAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
return HealthCheckResult.Healthy();
}
catch (Exception ex)
Expand Down
2 changes: 1 addition & 1 deletion src/HealthChecks.EventStore.gRPC/EventStoreHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,5 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
}
}

public void Dispose() => _client.Dispose();
public virtual void Dispose() => _client.Dispose();
}
2 changes: 2 additions & 0 deletions src/HealthChecks.Gremlin/GremlinHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
using var client = new GremlinClient(_server);
using var conn = new DriverRemoteConnection(client);
var g = Traversal().WithRemote(conn);
#pragma warning disable IDISP004 // Don't ignore created IDisposable
await g.Inject(0).Promise(t => t.Next()).ConfigureAwait(false);
#pragma warning restore IDISP004 // Don't ignore created IDisposable
}
catch (Exception ex)
{
Expand Down
11 changes: 10 additions & 1 deletion src/HealthChecks.InfluxDB/InfluxDBHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,20 @@ namespace HealthChecks.InfluxDB;

public class InfluxDBHealthCheck : IHealthCheck, IDisposable
{
#pragma warning disable IDISP008 // Don't assign member with injected and created disposables
private readonly InfluxDBClient _influxDbClient;
#pragma warning restore IDISP008 // Don't assign member with injected and created disposables
private readonly bool _ownsClient;

public InfluxDBHealthCheck(Func<InfluxDBClientOptions.Builder, InfluxDBClientOptions> _options)
{
_ownsClient = true;
_influxDbClient = new InfluxDBClient(_options.Invoke(InfluxDBClientOptions.Builder.CreateNew()));
}

public InfluxDBHealthCheck(InfluxDBClient influxDBClient)
{
_ownsClient = false;
_influxDbClient = influxDBClient;
}

Expand Down Expand Up @@ -44,5 +49,9 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
}
}

public void Dispose() => _influxDbClient.Dispose();
public virtual void Dispose()
{
if (_ownsClient)
_influxDbClient.Dispose();
}
}
2 changes: 1 addition & 1 deletion src/HealthChecks.Kafka/KafkaHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,5 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
}
}

public void Dispose() => _producer?.Dispose();
public virtual void Dispose() => _producer?.Dispose();
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,15 @@ public static IHealthChecksBuilder AddKubernetes(

Guard.ThrowIfNull(kubernetesHealthCheckBuilder.Configuration);

#pragma warning disable IDISP001 // Dispose created [by design, will be disposed by DisposalHostedService when app stops]
var client = new Kubernetes(kubernetesHealthCheckBuilder.Configuration);
var kubernetesChecksExecutor = new KubernetesChecksExecutor(client);
#pragma warning restore IDISP001 // Dispose created

builder.Services.AddHostedService(_ => new DisposalHostedService(client));

return builder.Add(new HealthCheckRegistration(
name ?? NAME,
sp => new KubernetesHealthCheck(kubernetesHealthCheckBuilder, kubernetesChecksExecutor),
_ => new KubernetesHealthCheck(kubernetesHealthCheckBuilder, client),
failureStatus,
tags,
timeout));
Expand Down
1 change: 1 addition & 0 deletions src/HealthChecks.Kubernetes/HealthChecks.Kubernetes.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<ItemGroup>
<PackageReference Include="KubernetesClient" Version="11.0.44" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.9" />
<Compile Include="../DisposalHostedService.cs" />
</ItemGroup>

</Project>
35 changes: 14 additions & 21 deletions src/HealthChecks.Kubernetes/KubernetesChecksExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,30 @@

namespace HealthChecks.Kubernetes;

public class KubernetesChecksExecutor
internal static class KubernetesChecksExecutor
{
private readonly k8s.Kubernetes _client;
private readonly Dictionary<Type, Func<KubernetesResourceCheck, CancellationToken, Task<(bool, string)>>> _handlers;

public KubernetesChecksExecutor(k8s.Kubernetes client)
private static readonly Dictionary<Type, Func<k8s.Kubernetes, KubernetesResourceCheck, CancellationToken, Task<(bool, string)>>> _handlers = new()
{
_client = Guard.ThrowIfNull(client);
_handlers = new Dictionary<Type, Func<KubernetesResourceCheck, CancellationToken, Task<(bool, string)>>>()
{
[typeof(V1Deployment)] = CheckDeploymentAsync,
[typeof(V1Service)] = CheckServiceAsync,
[typeof(V1Pod)] = CheckPodAsync
};
}
[typeof(V1Deployment)] = CheckDeploymentAsync,
[typeof(V1Service)] = CheckServiceAsync,
[typeof(V1Pod)] = CheckPodAsync
};

public Task<(bool, string)> CheckAsync(KubernetesResourceCheck resourceCheck, CancellationToken cancellationToken)
public static Task<(bool, string)> CheckAsync(k8s.Kubernetes client, KubernetesResourceCheck resourceCheck, CancellationToken cancellationToken)
{
var handler = _handlers[resourceCheck.ResourceType];
return handler?.Invoke(resourceCheck, cancellationToken) ??
return handler?.Invoke(client, resourceCheck, cancellationToken) ??
throw new InvalidOperationException(
$"No handler registered for type {resourceCheck.ResourceType.Name}");
}

private async Task<(bool, string)> CheckDeploymentAsync(KubernetesResourceCheck resourceCheck, CancellationToken cancellationToken)
private static async Task<(bool, string)> CheckDeploymentAsync(k8s.Kubernetes client, KubernetesResourceCheck resourceCheck, CancellationToken cancellationToken)
{
var tsc = new TaskCompletionSource<(bool, string)>();

try
{
var result = await _client.AppsV1.ReadNamespacedDeploymentStatusWithHttpMessagesAsync(resourceCheck.Name,
using var result = await client.AppsV1.ReadNamespacedDeploymentStatusWithHttpMessagesAsync(resourceCheck.Name,
resourceCheck.Namespace, cancellationToken: cancellationToken).ConfigureAwait(false);

tsc.SetResult((resourceCheck.Check(result.Body), resourceCheck.Name));
Expand All @@ -46,12 +39,12 @@ public KubernetesChecksExecutor(k8s.Kubernetes client)
return await tsc.Task.ConfigureAwait(false);
}

private async Task<(bool, string)> CheckPodAsync(KubernetesResourceCheck resourceCheck, CancellationToken cancellationToken)
private static async Task<(bool, string)> CheckPodAsync(k8s.Kubernetes client, KubernetesResourceCheck resourceCheck, CancellationToken cancellationToken)
{
var tsc = new TaskCompletionSource<(bool, string)>();
try
{
var result = await _client.CoreV1.ReadNamespacedPodStatusWithHttpMessagesAsync(resourceCheck.Name,
using var result = await client.CoreV1.ReadNamespacedPodStatusWithHttpMessagesAsync(resourceCheck.Name,
resourceCheck.Namespace, cancellationToken: cancellationToken).ConfigureAwait(false);

tsc.SetResult((resourceCheck.Check(result.Body), resourceCheck.Name));
Expand All @@ -65,12 +58,12 @@ public KubernetesChecksExecutor(k8s.Kubernetes client)
return await tsc.Task.ConfigureAwait(false);
}

private async Task<(bool, string)> CheckServiceAsync(KubernetesResourceCheck resourceCheck, CancellationToken cancellationToken)
private static async Task<(bool, string)> CheckServiceAsync(k8s.Kubernetes client, KubernetesResourceCheck resourceCheck, CancellationToken cancellationToken)
{
var tsc = new TaskCompletionSource<(bool, string)>();
try
{
var result = await _client.CoreV1.ReadNamespacedServiceStatusWithHttpMessagesAsync(resourceCheck.Name,
using var result = await client.CoreV1.ReadNamespacedServiceStatusWithHttpMessagesAsync(resourceCheck.Name,
resourceCheck.Namespace, cancellationToken: cancellationToken).ConfigureAwait(false);

tsc.SetResult((resourceCheck.Check(result.Body), resourceCheck.Name));
Expand Down
9 changes: 4 additions & 5 deletions src/HealthChecks.Kubernetes/KubernetesHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ namespace HealthChecks.Kubernetes;
public class KubernetesHealthCheck : IHealthCheck
{
private readonly KubernetesHealthCheckBuilder _builder;
private readonly KubernetesChecksExecutor _kubernetesChecksExecutor;
private readonly k8s.Kubernetes _client;

public KubernetesHealthCheck(KubernetesHealthCheckBuilder builder,
KubernetesChecksExecutor kubernetesChecksExecutor)
public KubernetesHealthCheck(KubernetesHealthCheckBuilder builder, k8s.Kubernetes client)
{
_builder = Guard.ThrowIfNull(builder);
_kubernetesChecksExecutor = Guard.ThrowIfNull(kubernetesChecksExecutor);
_client = Guard.ThrowIfNull(client);
}

/// <inheritdoc />
Expand All @@ -23,7 +22,7 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
{
foreach (var item in _builder.Options.Registrations)
{
checks.Add(_kubernetesChecksExecutor.CheckAsync(item, cancellationToken));
checks.Add(KubernetesChecksExecutor.CheckAsync(_client, item, cancellationToken));
}

var results = await Task.WhenAll(checks).PreserveMultipleExceptions().ConfigureAwait(false);
Expand Down
2 changes: 2 additions & 0 deletions src/HealthChecks.Nats/NatsHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, Canc
IConnection? connection = _connection;
if (connection == null)
{
#pragma warning disable IDISP001 // Dispose created [false positive, https://github.com/DotNetAnalyzers/IDisposableAnalyzers/issues/515]
connection = CreateConnection(_options);
#pragma warning restore IDISP001 // Dispose created
var exchanged = Interlocked.CompareExchange(ref _connection, connection, null);
if (exchanged != null) // was set by other thread
{
Expand Down
2 changes: 2 additions & 0 deletions src/HealthChecks.Network/FtpHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
{
var ftpRequest = CreateFtpWebRequest(host, createFile, credentials);

#pragma warning disable IDISP004 // Don't ignore created IDisposable
using var ftpResponse = (FtpWebResponse)await ftpRequest.GetResponseAsync().WithCancellationTokenAsync(cancellationToken).ConfigureAwait(false);
#pragma warning restore IDISP004 // Don't ignore created IDisposable

if (ftpResponse.StatusCode != FtpStatusCode.PathnameCreated && ftpResponse.StatusCode != FtpStatusCode.ClosingData)
{
Expand Down
2 changes: 2 additions & 0 deletions src/HealthChecks.Network/SftpConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ public SftpConfigurationBuilder AddPrivateKeyAuthentication(string privateKey, s
memoryStream.Write(keyBytes, 0, keyBytes.Length);
memoryStream.Seek(0, SeekOrigin.Begin);

#pragma warning disable IDISP001 // Dispose created
var privateKeyFile = new PrivateKeyFile(memoryStream, passphrase);
#pragma warning restore IDISP001 // Dispose created

AuthenticationMethods.Add(new PrivateKeyAuthenticationMethod(_userName, privateKeyFile));

Expand Down
11 changes: 5 additions & 6 deletions src/HealthChecks.Network/SslHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
continue;
}

var certificate = await GetSslCertificateAsync(tcpClient, host).ConfigureAwait(false);
using var certificate = await GetSslCertificateAsync(tcpClient, host).ConfigureAwait(false);

if (certificate is null || !certificate.Verify())
{
Expand Down Expand Up @@ -73,7 +73,10 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context

private async Task<X509Certificate2?> GetSslCertificateAsync(TcpClient client, string host)
{
var ssl = new SslStream(client.GetStream(), false, new RemoteCertificateValidationCallback((sender, cert, ca, sslPolicyErrors) => sslPolicyErrors == SslPolicyErrors.None), null);
// https://github.com/DotNetAnalyzers/IDisposableAnalyzers/issues/513
#pragma warning disable IDISP004 // Don't ignore created IDisposable
using var ssl = new SslStream(client.GetStream(), false, new RemoteCertificateValidationCallback((sender, cert, ca, sslPolicyErrors) => sslPolicyErrors == SslPolicyErrors.None), null);
#pragma warning restore IDISP004 // Don't ignore created IDisposable

try
{
Expand All @@ -85,9 +88,5 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
{
return null;
}
finally
{
ssl.Close();
}
}
}
1 change: 1 addition & 0 deletions src/HealthChecks.RavenDB/RavenDBHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public RavenDBHealthCheck(RavenDBOptions options)
}

/// <inheritdoc />
[System.Diagnostics.CodeAnalysis.SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP004:Don't ignore created IDisposable", Justification = "DocumentStore instances are static")]
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
Expand Down
2 changes: 2 additions & 0 deletions src/HealthChecks.Redis/RedisHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
if (_redisConnectionString is not null)
{
_connections.TryRemove(_redisConnectionString, out var connection);
#pragma warning disable IDISP007 // Don't dispose injected [false positive here]
connection?.Dispose();
#pragma warning restore IDISP007 // Don't dispose injected
}
return new HealthCheckResult(context.Registration.FailureStatus, exception: ex);
}
Expand Down
2 changes: 1 addition & 1 deletion src/HealthChecks.SendGrid/SendGridHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
{
try
{
var httpClient = _httpClientFactory.CreateClient(SendGridHealthCheckExtensions.NAME);
using var httpClient = _httpClientFactory.CreateClient(SendGridHealthCheckExtensions.NAME);

var client = new SendGridClient(httpClient, _apiKey);
var from = new EmailAddress(MAIL_ADDRESS, MAIL_ADDRESS_NAME);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace HealthChecks.UI.K8s.Operator.Operator
{
internal class ClusterServiceWatcher
internal sealed class ClusterServiceWatcher : IDisposable
{
private readonly IKubernetes _client;
private readonly ILogger<K8sOperator> _logger;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public static async Task PushNotification( //TODO: rename public API
Uri = address
};

var client = httpClientFactory.CreateClient();
using var client = httpClientFactory.CreateClient();
try
{
string type = healthCheck.Type.ToString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace HealthChecks.UI.K8s.Operator
{
internal class NamespacedServiceWatcher : IDisposable
internal sealed class NamespacedServiceWatcher : IDisposable
{
private readonly IKubernetes _client;
private readonly ILogger<K8sOperator> _logger;
Expand Down
Loading

0 comments on commit cc44a46

Please sign in to comment.