Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/Couchbase.Analytics/Cluster.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ public Database Database(string databaseName)

public void Dispose()
{
// TODO release managed resources here
if (_serviceProvider is IDisposable disposableProvider)
{
disposableProvider.Dispose();
}
}
}
35 changes: 35 additions & 0 deletions src/Couchbase.Analytics/Internal/DI/CouchbaseServiceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* ************************************************************/
#endregion

using System;
using System.Collections.ObjectModel;

namespace Couchbase.AnalyticsClient.Internal.DI;
Expand Down Expand Up @@ -80,4 +81,38 @@ public bool IsService(Type serviceType)

return false;
}

private bool _disposed;

public void Dispose()
{
if (_disposed)
{
return;
}

_disposed = true;

var exceptions = new List<Exception>();

foreach (var factory in _services.Values)
{
if (factory is IDisposable disposableFactory)
{
try
{
disposableFactory.Dispose();
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
}

if (exceptions.Count > 0)
{
throw new AggregateException(exceptions);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ namespace Couchbase.AnalyticsClient.Internal.DI;
/// <summary>
/// Extends <see cref="IServiceProvider"/> with a method to test for service registration.
/// </summary>
internal interface ICouchbaseServiceProvider : IServiceProvider
internal interface ICouchbaseServiceProvider : IServiceProvider, IDisposable
{
/// <summary>
/// Determines if the specified service type is available from the <see cref="ICouchbaseServiceProvider"/>.
Expand Down
2 changes: 1 addition & 1 deletion src/Couchbase.Analytics/Internal/DI/IServiceFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ namespace Couchbase.AnalyticsClient.Internal.DI;
/// <summary>
/// A factory capable of returning a service.
/// </summary>
internal interface IServiceFactory
internal interface IServiceFactory : IDisposable
{
/// <summary>
/// Initializes the factory, making it owned by the given <see cref="IServiceProvider"/>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,29 @@ private object Factory(Type requestedType)

return constructor.Invoke(constructorArgs);
}

public void Dispose()
{
var exceptions = new List<Exception>();

foreach (var singleton in _singletons.Values)
{
if (singleton is IDisposable disposable)
{
try
{
disposable.Dispose();
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
}

if (exceptions.Count > 0)
{
throw new AggregateException(exceptions);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* ************************************************************/
#endregion

using System;
using System.Diagnostics.CodeAnalysis;
using Couchbase.AnalyticsClient.Utils;

Expand Down Expand Up @@ -108,4 +109,12 @@ object Factory(IServiceProvider serviceProvider)

return Factory;
}

public void Dispose()
{
if (_singleton is IDisposable disposable)
{
disposable.Dispose();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,9 @@ object Factory(IServiceProvider serviceProvider)
return constructor.Invoke(constructorArgs);
}
}

public void Dispose()
{
// Transient factories do not manage singleton resources, so disposal is a no-op
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -223,5 +223,10 @@ private static async Task DisposeAfterDelayAsync(AuthenticationHandler handler)
handler.Dispose();
}

public void Dispose()
{
_sharedHandler?.Dispose();
}

public HttpCompletionOption DefaultCompletionOption { get; set; } = HttpCompletionOption.ResponseHeadersRead;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ namespace Couchbase.AnalyticsClient.Internal.HTTP;
/// Creates an <see cref="HttpClient"/> which may be safely configured and disposed, but while
/// reusing inner handlers for connection pooling and HTTP keep-alives.
/// </summary>
internal interface ICouchbaseHttpClientFactory
internal interface ICouchbaseHttpClientFactory : IDisposable
{
/// <summary>
/// Creates an <see cref="HttpClient"/> which may be safely configured and disposed, but while
Expand Down
28 changes: 28 additions & 0 deletions tests/Couchbase.Analytics.UnitTests/ClusterDisposalTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using Couchbase.AnalyticsClient;
using Couchbase.AnalyticsClient.HTTP;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;

namespace Couchbase.Analytics.UnitTests;

public class ClusterDisposalTests
{
[Fact]
public void Cluster_Dispose_DisposesAllRegisteredSingletons()
{
var mockLoggerFactory = new Mock<ILoggerFactory>();

var cluster = Cluster.Create(
"http://localhost:8095",
new Credential("Administrator", "password"),
opts => opts.WithLogging(mockLoggerFactory.Object));

// Trigger the top-level Dispose, which must cascade down the DI structural tree
// and aggressively purge the singleton cache.
cluster.Dispose();

mockLoggerFactory.Verify(x => x.Dispose(), Times.Once, "The Cluster failed to actively dispose its registered singleton resources (like the logging framework and SocketsHttpHandler) during destruction!");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using Couchbase.AnalyticsClient.Internal.DI;
using Moq;
using Xunit;

namespace Couchbase.Analytics.UnitTests.Internal.DI;

public class CouchbaseServiceProviderTests
{
[Fact]
public void Dispose_IteratesAndDisposesAllDisposableFactories()
{
// Arrange
var mockNonDisposableFactory = new Mock<IServiceFactory>();
var mockDisposableFactory = new Mock<IServiceFactory>();

// Explicitly project the second mock to support the IDisposable interface
var disposableInterface = mockDisposableFactory.As<IDisposable>();

var services = new Dictionary<Type, IServiceFactory>
{
{ typeof(string), mockNonDisposableFactory.Object },
{ typeof(int), mockDisposableFactory.Object }
};

var provider = new CouchbaseServiceProvider(services);

// Act
provider.Dispose();

// Assert
// The normal factory should simply receive its baseline Initialize call from the constructor, and nothing else.
mockNonDisposableFactory.Verify(f => f.Initialize(It.IsAny<IServiceProvider>()), Times.Once);

// ...and definitively fires exactly once against any factory holding the IDisposable pattern.
disposableInterface.Verify(d => d.Dispose(), Times.Once);
}

[Fact]
public void Dispose_IsIdempotent()
{
// Arrange
var mockDisposableFactory = new Mock<IServiceFactory>();
var disposableInterface = mockDisposableFactory.As<IDisposable>();

var services = new Dictionary<Type, IServiceFactory>
{
{ typeof(int), mockDisposableFactory.Object }
};

var provider = new CouchbaseServiceProvider(services);

// Act
provider.Dispose();
provider.Dispose(); // Call second time
provider.Dispose(); // Call third time

// Assert
// Due to the _disposed flag, multiple explicit invocations must collapse harmlessly,
// firing downstream teardowns EXACTLY once.
disposableInterface.Verify(d => d.Dispose(), Times.Once);
}

[Fact]
public void Dispose_ExceptionInOneFactory_DoesNotPreventDisposalOfOthers()
{
// Arrange
var mockFailingFactory = new Mock<IServiceFactory>();
var failingDisposable = mockFailingFactory.As<IDisposable>();
failingDisposable.Setup(d => d.Dispose()).Throws(new InvalidOperationException("Boom"));

var mockWorkingFactory = new Mock<IServiceFactory>();
var workingDisposable = mockWorkingFactory.As<IDisposable>();

var services = new Dictionary<Type, IServiceFactory>
{
{ typeof(int), mockFailingFactory.Object },
{ typeof(string), mockWorkingFactory.Object }
};

var provider = new CouchbaseServiceProvider(services);

// Act
var aggregateEx = Assert.Throws<AggregateException>(() => provider.Dispose());

// Assert
// We guarantee the loop continued executing despite the explosion.
Assert.Contains(aggregateEx.InnerExceptions, ex => ex is InvalidOperationException && ex.Message == "Boom");
workingDisposable.Verify(d => d.Dispose(), Times.Once);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using Couchbase.AnalyticsClient.Internal.DI;
using Moq;
using Xunit;

namespace Couchbase.Analytics.UnitTests.Internal.DI;

public interface IFakeGenericService<T> : IDisposable { }

public class FakeGenericService<T> : IFakeGenericService<T>
{
public static Action? OnDispose { get; set; }

public FakeGenericService() { }

public void Dispose()
{
OnDispose?.Invoke();
}
}

public class SingletonGenericServiceFactoryTests
{
[Fact]
public void SingletonGenericServiceFactory_Dispose_DisposesAllCachedPermutations()
{
// Arrange
var factory = new SingletonGenericServiceFactory(typeof(FakeGenericService<>));

var mockServiceProvider = new Mock<IServiceProvider>();
factory.Initialize(mockServiceProvider.Object);

// Populate the concurrent dictionary with different permutations of the generic
factory.CreateService(typeof(IFakeGenericService<int>));
factory.CreateService(typeof(IFakeGenericService<string>));
factory.CreateService(typeof(IFakeGenericService<double>));

int disposeCount = 0;
FakeGenericService<int>.OnDispose = () => disposeCount++;
FakeGenericService<string>.OnDispose = () => disposeCount++;
FakeGenericService<double>.OnDispose = () => disposeCount++;

// Act
factory.Dispose();

// Assert
// The factory should naturally iterate and call Dispose exactly 3 times (once per instantiated T)
Assert.Equal(3, disposeCount);

// Cleanup statics
FakeGenericService<int>.OnDispose = null;
FakeGenericService<string>.OnDispose = null;
FakeGenericService<double>.OnDispose = null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using Couchbase.AnalyticsClient.Internal.DI;
using Moq;
using Xunit;

namespace Couchbase.Analytics.UnitTests.Internal.DI;

public class SingletonServiceFactoryTests
{
[Fact]
public void SingletonServiceFactory_Dispose_DisposesInnerSingleton()
{
// Arrange
var mockDisposable = new Mock<IDisposable>();
var factory = new SingletonServiceFactory(mockDisposable.Object);

// Act
factory.Dispose();

// Assert
mockDisposable.Verify(d => d.Dispose(), Times.Once);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using Couchbase.AnalyticsClient.Internal.DI;
using Moq;
using Xunit;

namespace Couchbase.Analytics.UnitTests.Internal.DI;

public class TransientServiceFactoryTests
{
[Fact]
public void TransientServiceFactory_Dispose_DoesNotDisposeGeneratedInstances()
{
// Arrange
var mockDisposable = new Mock<IDisposable>();
var factory = new TransientServiceFactory(_ => mockDisposable.Object);

var mockServiceProvider = new Mock<IServiceProvider>();
factory.Initialize(mockServiceProvider.Object);

// Act
// We simulate the lifetime: The user asks for a Transient object, then later the Cluster shuts down entirely.
var instance = factory.CreateService(typeof(IDisposable));

factory.Dispose();

// Assert
// We explicitly confirm that the Transient factory completely ignores the instances
// it generates, thus avoiding long-term memory leaks in the Cluster's DI container!
Assert.NotNull(instance);
mockDisposable.Verify(d => d.Dispose(), Times.Never);
}
}
Loading
Loading