diff --git a/src/Speckle.Sdk/SQLite/SQLiteJsonCacheManager.cs b/src/Speckle.Sdk/SQLite/SQLiteJsonCacheManager.cs index 0c3d89f7..b08a83ed 100644 --- a/src/Speckle.Sdk/SQLite/SQLiteJsonCacheManager.cs +++ b/src/Speckle.Sdk/SQLite/SQLiteJsonCacheManager.cs @@ -13,15 +13,18 @@ public sealed class SqLiteJsonCacheManager : ISqLiteJsonCacheManager { private readonly CacheDbCommandPool _pool; + public string Path { get; } + public static ISqLiteJsonCacheManager FromMemory(int concurrency) => new SqLiteJsonCacheManager(concurrency); private SqLiteJsonCacheManager(int concurrency) { + Path = ":memory:"; //disable pooling as we pool ourselves var builder = new SqliteConnectionStringBuilder { Pooling = false, - DataSource = ":memory:", + DataSource = Path, Cache = SqliteCacheMode.Shared, Mode = SqliteOpenMode.Memory, }; @@ -34,8 +37,9 @@ public sealed class SqLiteJsonCacheManager : ISqLiteJsonCacheManager private SqLiteJsonCacheManager(string path, int concurrency) { + Path = path; //disable pooling as we pool ourselves - var builder = new SqliteConnectionStringBuilder { Pooling = false, DataSource = path }; + var builder = new SqliteConnectionStringBuilder { Pooling = false, DataSource = Path }; _pool = new CacheDbCommandPool(builder.ToString(), concurrency); Initialize(); } diff --git a/src/Speckle.Sdk/Serialisation/V2/MemoryJsonCacheManager.cs b/src/Speckle.Sdk/Serialisation/V2/MemoryJsonCacheManager.cs index 26e142be..21395e5f 100644 --- a/src/Speckle.Sdk/Serialisation/V2/MemoryJsonCacheManager.cs +++ b/src/Speckle.Sdk/Serialisation/V2/MemoryJsonCacheManager.cs @@ -7,6 +7,10 @@ namespace Speckle.Sdk.Serialisation.V2; public class MemoryJsonCacheManager(ConcurrentDictionary jsonCache) : ISqLiteJsonCacheManager #pragma warning restore CA1063 { +#pragma warning disable CA1065 + public string Path => "MemoryJsonCacheManager"; +#pragma warning restore CA1065 + public IReadOnlyCollection<(string Id, string Json)> GetAllObjects() => jsonCache.Select(x => (x.Key.Value, x.Value.Value)).ToList(); diff --git a/src/Speckle.Sdk/Serialisation/V2/Send/ObjectSaverFactory.cs b/src/Speckle.Sdk/Serialisation/V2/Send/ObjectSaverFactory.cs new file mode 100644 index 00000000..96ddbcb3 --- /dev/null +++ b/src/Speckle.Sdk/Serialisation/V2/Send/ObjectSaverFactory.cs @@ -0,0 +1,50 @@ +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; +using Speckle.InterfaceGenerator; +using Speckle.Sdk.SQLite; +using Speckle.Sdk.Transports; + +namespace Speckle.Sdk.Serialisation.V2.Send; + +public partial interface IObjectSaverFactory : IDisposable; + +[GenerateAutoInterface] +public sealed class ObjectSaverFactory(ILoggerFactory loggerFactory) : IObjectSaverFactory +{ + private readonly ConcurrentDictionary _savers = new(); + + public IObjectSaver Create( + IServerObjectManager serverObjectManager, + ISqLiteJsonCacheManager sqLiteJsonCacheManager, + IProgress? progress, + CancellationToken cancellationToken, + SerializeProcessOptions? options = null + ) + { + if (!_savers.TryGetValue(sqLiteJsonCacheManager.Path, out var saver)) + { + saver = new ObjectSaver( + progress, + sqLiteJsonCacheManager, + serverObjectManager, + loggerFactory.CreateLogger(), + cancellationToken, + options + ); + _savers.TryAdd(sqLiteJsonCacheManager.Path, saver); + } + + return saver; + } + + [AutoInterfaceIgnore] + public void Dispose() + { + foreach (var pool in _savers) + { + pool.Value.Dispose(); + } + + _savers.Clear(); + } +} diff --git a/src/Speckle.Sdk/Serialisation/V2/Send/SerializeProcess.cs b/src/Speckle.Sdk/Serialisation/V2/Send/SerializeProcess.cs index 0254921d..35cb3731 100644 --- a/src/Speckle.Sdk/Serialisation/V2/Send/SerializeProcess.cs +++ b/src/Speckle.Sdk/Serialisation/V2/Send/SerializeProcess.cs @@ -86,7 +86,6 @@ public sealed class SerializeProcess( await WaitForSchedulerCompletion().ConfigureAwait(false); await _highest.DisposeAsync().ConfigureAwait(false); await _belowNormal.DisposeAsync().ConfigureAwait(false); - objectSaver.Dispose(); _processSource.Dispose(); } diff --git a/src/Speckle.Sdk/Serialisation/V2/SerializeProcessFactory.cs b/src/Speckle.Sdk/Serialisation/V2/SerializeProcessFactory.cs index c1428bf9..2bb38bac 100644 --- a/src/Speckle.Sdk/Serialisation/V2/SerializeProcessFactory.cs +++ b/src/Speckle.Sdk/Serialisation/V2/SerializeProcessFactory.cs @@ -13,6 +13,7 @@ public class SerializeProcessFactory( IObjectSerializerFactory objectSerializerFactory, ISqLiteJsonCacheManagerFactory sqLiteJsonCacheManagerFactory, IServerObjectManagerFactory serverObjectManagerFactory, + IObjectSaverFactory objectSaverFactory, ILoggerFactory loggerFactory ) : ISerializeProcessFactory { @@ -39,13 +40,7 @@ public class SerializeProcessFactory( ) => new SerializeProcess( progress, - new ObjectSaver( - progress, - sqLiteJsonCacheManager, - serverObjectManager, - loggerFactory.CreateLogger(), - cancellationToken - ), + objectSaverFactory.Create(serverObjectManager, sqLiteJsonCacheManager, progress, cancellationToken, options), baseChildFinder, new BaseSerializer(sqLiteJsonCacheManager, objectSerializerFactory), loggerFactory, diff --git a/src/Speckle.Sdk/ServiceRegistration.cs b/src/Speckle.Sdk/ServiceRegistration.cs index 8f8066ae..106be8bd 100644 --- a/src/Speckle.Sdk/ServiceRegistration.cs +++ b/src/Speckle.Sdk/ServiceRegistration.cs @@ -97,6 +97,8 @@ public static class ServiceRegistration typeof(Client) ); serviceCollection.AddMatchingInterfacesAsTransient(typeof(GraphQLRetry).Assembly); + //we want to make object savers be singletons per stream so needs a singleton factory + serviceCollection.AddSingleton(); return serviceCollection; } diff --git a/tests/Speckle.Sdk.Serialization.Tests/DataObjectTests.cs b/tests/Speckle.Sdk.Serialization.Tests/DataObjectTests.cs index da6fdef8..90435f58 100644 --- a/tests/Speckle.Sdk.Serialization.Tests/DataObjectTests.cs +++ b/tests/Speckle.Sdk.Serialization.Tests/DataObjectTests.cs @@ -41,7 +41,7 @@ public class DataObjectTests new DummyServerObjectManager(), null, default, - new SerializeProcessOptions(true, true, false, true) + new SerializeProcessOptions(false, false, false, true) ); await serializeProcess.Serialize(x); await VerifyJson(json.Single().Value.Value).UseParameters(type); diff --git a/tests/Speckle.Sdk.Serialization.Tests/DetachedTests.cs b/tests/Speckle.Sdk.Serialization.Tests/DetachedTests.cs index a8664172..62568110 100644 --- a/tests/Speckle.Sdk.Serialization.Tests/DetachedTests.cs +++ b/tests/Speckle.Sdk.Serialization.Tests/DetachedTests.cs @@ -41,7 +41,7 @@ public class DetachedTests objects, null, default, - new SerializeProcessOptions(false, false, true, true) + new SerializeProcessOptions(false, false, false, true) ); await serializeProcess.Serialize(@base); @@ -123,7 +123,7 @@ public class DetachedTests objects, null, default, - new SerializeProcessOptions(false, false, true, true) { MaxParallelism = 1, MaxHttpSendBatchSize = 1 } + new SerializeProcessOptions(false, false, false, true) { MaxParallelism = 1, MaxHttpSendBatchSize = 1 } ); var results = await serializeProcess.Serialize(@base); @@ -150,7 +150,7 @@ public class DetachedTests objects, null, default, - new SerializeProcessOptions(false, false, true, true) { MaxParallelism = 1, MaxHttpSendBatchSize = 1 } + new SerializeProcessOptions(false, false, false, true) { MaxParallelism = 1, MaxHttpSendBatchSize = 1 } ); var results = await serializeProcess.Serialize(@base); @@ -172,7 +172,7 @@ public class DetachedTests objects, null, default, - new SerializeProcessOptions(false, false, true, true) { MaxParallelism = 1, MaxHttpSendBatchSize = 1 } + new SerializeProcessOptions(false, false, false, true) { MaxParallelism = 1, MaxHttpSendBatchSize = 1 } ); var results = await serializeProcess.Serialize(@base); @@ -239,7 +239,7 @@ public class DetachedTests objects, null, default, - new SerializeProcessOptions(false, false, true, true) + new SerializeProcessOptions(false, false, false, true) ); var results = await serializeProcess.Serialize(@base); @@ -272,7 +272,7 @@ public class DetachedTests objects, null, default, - new SerializeProcessOptions(false, false, true, true) + new SerializeProcessOptions(false, false, false, true) ); var results = await serializeProcess.Serialize(@base); await VerifyJsonDictionary(objects); @@ -373,6 +373,9 @@ public class DummyServerObjectManager : IServerObjectManager public class DummySendCacheManager(Dictionary objects) : ISqLiteJsonCacheManager { +#pragma warning disable CA1065 + public string Path => throw new NotImplementedException(); +#pragma warning restore CA1065 public void Dispose() { } public IReadOnlyCollection<(string, string)> GetAllObjects() => throw new NotImplementedException(); diff --git a/tests/Speckle.Sdk.Serialization.Tests/DummyCancellationSqLiteSendManager.cs b/tests/Speckle.Sdk.Serialization.Tests/DummyCancellationSqLiteSendManager.cs index 2d6ca588..1244b303 100644 --- a/tests/Speckle.Sdk.Serialization.Tests/DummyCancellationSqLiteSendManager.cs +++ b/tests/Speckle.Sdk.Serialization.Tests/DummyCancellationSqLiteSendManager.cs @@ -4,6 +4,9 @@ namespace Speckle.Sdk.Serialization.Tests; public class DummyCancellationSqLiteSendManager : ISqLiteJsonCacheManager { +#pragma warning disable CA1065 + public string Path => throw new NotImplementedException(); +#pragma warning restore CA1065 public string? GetObject(string id) => null; public void SaveObject(string id, string json) => throw new NotImplementedException(); diff --git a/tests/Speckle.Sdk.Serialization.Tests/Framework/ExceptionSendCacheManager.cs b/tests/Speckle.Sdk.Serialization.Tests/Framework/ExceptionSendCacheManager.cs index 5572aa02..0f3df346 100644 --- a/tests/Speckle.Sdk.Serialization.Tests/Framework/ExceptionSendCacheManager.cs +++ b/tests/Speckle.Sdk.Serialization.Tests/Framework/ExceptionSendCacheManager.cs @@ -4,6 +4,7 @@ namespace Speckle.Sdk.Serialization.Tests.Framework; public class ExceptionSendCacheManager(bool? hasObject = null, int? exceptionsAfter = null) : ISqLiteJsonCacheManager { + public string Path => "ExceptionSendCacheManager"; private readonly object _lock = new(); private int _count; diff --git a/tests/Speckle.Sdk.Serialization.Tests/ObjectSaverFactoryTests.cs b/tests/Speckle.Sdk.Serialization.Tests/ObjectSaverFactoryTests.cs new file mode 100644 index 00000000..253c52a2 --- /dev/null +++ b/tests/Speckle.Sdk.Serialization.Tests/ObjectSaverFactoryTests.cs @@ -0,0 +1,90 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Moq; +using Speckle.Sdk.SQLite; +using Speckle.Sdk.Testing; + +namespace Speckle.Sdk.Serialisation.V2.Send.Tests; + +public class ObjectSaverFactoryTests : MoqTest +{ + private readonly Mock _loggerFactoryMock; + private readonly Mock> _loggerMock; + private readonly ObjectSaverFactory _factory; + + public ObjectSaverFactoryTests() + { + _loggerFactoryMock = Create(); + _loggerMock = Create>(); + _factory = new ObjectSaverFactory(_loggerFactoryMock.Object); + } + + public override void Dispose() + { + _factory.Dispose(); + base.Dispose(); + } + + [Fact] + public void Create_ShouldReturnObjectSaverInstance() + { + _loggerFactoryMock.Setup(f => f.CreateLogger(typeof(ObjectSaver).FullName)).Returns(_loggerMock.Object); + var cacheManagerMock = Create(); + cacheManagerMock.Setup(x => x.Dispose()); + cacheManagerMock.SetupGet(c => c.Path).Returns("/tmp/test1.db"); + + var saver = _factory.Create( + Create().Object, + cacheManagerMock.Object, + null, + CancellationToken.None + ); + + saver.Should().NotBeNull(); + } + + [Fact] + public void Create_ShouldReturnSameInstanceForSamePath() + { + _loggerFactoryMock.Setup(f => f.CreateLogger(typeof(ObjectSaver).FullName)).Returns(_loggerMock.Object); + var cacheManagerMock = Create(); + cacheManagerMock.Setup(x => x.Dispose()); + cacheManagerMock.SetupGet(c => c.Path).Returns("/tmp/test2.db"); + + var saver1 = _factory.Create( + Create().Object, + cacheManagerMock.Object, + null, + CancellationToken.None + ); + var saver2 = _factory.Create( + Create().Object, + cacheManagerMock.Object, + null, + CancellationToken.None + ); + + saver1.Should().BeSameAs(saver2); + } + + [Fact] + public void Dispose_ShouldDisposeAllSavers() + { + var saverMock1 = Create(); + _factory + .GetType() + .GetField("_savers", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.SetValue( + _factory, + new System.Collections.Concurrent.ConcurrentDictionary( + new[] + { + new System.Collections.Generic.KeyValuePair("/tmp/test3.db", saverMock1.Object), + } + ) + ); + saverMock1.Setup(x => x.Dispose()); + + _factory.Dispose(); + } +} diff --git a/tests/Speckle.Sdk.Testing/Framework/DummySqLiteReceiveManager.cs b/tests/Speckle.Sdk.Testing/Framework/DummySqLiteReceiveManager.cs index fcfe6832..01109410 100644 --- a/tests/Speckle.Sdk.Testing/Framework/DummySqLiteReceiveManager.cs +++ b/tests/Speckle.Sdk.Testing/Framework/DummySqLiteReceiveManager.cs @@ -5,6 +5,9 @@ namespace Speckle.Sdk.Testing.Framework; public sealed class DummySqLiteReceiveManager(IReadOnlyDictionary savedObjects) : ISqLiteJsonCacheManager { +#pragma warning disable CA1065 + public string Path => throw new NotImplementedException(); +#pragma warning restore CA1065 public void Dispose() { } public IReadOnlyCollection<(string, string)> GetAllObjects() => throw new NotImplementedException(); diff --git a/tests/Speckle.Sdk.Testing/Framework/DummySqLiteSendManager.cs b/tests/Speckle.Sdk.Testing/Framework/DummySqLiteSendManager.cs index b1425dd6..97e3f0f5 100644 --- a/tests/Speckle.Sdk.Testing/Framework/DummySqLiteSendManager.cs +++ b/tests/Speckle.Sdk.Testing/Framework/DummySqLiteSendManager.cs @@ -4,6 +4,8 @@ namespace Speckle.Sdk.Testing.Framework; public class DummySqLiteSendManager : ISqLiteJsonCacheManager { + public string Path => "DummySqLiteSendManager"; + public string? GetObject(string id) => throw new NotImplementedException(); public void SaveObject(string id, string json) => throw new NotImplementedException(); diff --git a/tests/Speckle.Sdk.Testing/MoqTest.cs b/tests/Speckle.Sdk.Testing/MoqTest.cs index 34e8391b..bc3ca8fa 100644 --- a/tests/Speckle.Sdk.Testing/MoqTest.cs +++ b/tests/Speckle.Sdk.Testing/MoqTest.cs @@ -8,7 +8,7 @@ public abstract class MoqTest : IDisposable { protected MoqTest() => Repository = new(MockBehavior.Strict); - public void Dispose() => Repository.VerifyAll(); + public virtual void Dispose() => Repository.VerifyAll(); protected MockRepository Repository { get; private set; } = new(MockBehavior.Strict); diff --git a/tests/Speckle.Sdk.Tests.Unit/Serialisation/SerializeProcessRecordExceptionTests.cs b/tests/Speckle.Sdk.Tests.Unit/Serialisation/SerializeProcessRecordExceptionTests.cs index 1e855dd7..a69a5f76 100644 --- a/tests/Speckle.Sdk.Tests.Unit/Serialisation/SerializeProcessRecordExceptionTests.cs +++ b/tests/Speckle.Sdk.Tests.Unit/Serialisation/SerializeProcessRecordExceptionTests.cs @@ -21,7 +21,6 @@ public class SerializeProcessRecordExceptionTests : MoqTest .Setup(f => f.CreateLogger("Speckle.Sdk.Serialisation.V2.PriorityScheduler")) .Returns(Create>().Object); var objectSaverMock = Create(); - objectSaverMock.Setup(x => x.Dispose()); var baseChildFinderMock = Create(); var baseSerializerMock = Create(); using var cts = new CancellationTokenSource(); @@ -57,7 +56,6 @@ public class SerializeProcessRecordExceptionTests : MoqTest .Setup(f => f.CreateLogger("Speckle.Sdk.Serialisation.V2.PriorityScheduler")) .Returns(Create>().Object); var objectSaverMock = Create(); - objectSaverMock.Setup(x => x.Dispose()); var baseChildFinderMock = Create(); var baseSerializerMock = Create(); using var cts = new CancellationTokenSource(); @@ -88,7 +86,6 @@ public class SerializeProcessRecordExceptionTests : MoqTest .Setup(f => f.CreateLogger("Speckle.Sdk.Serialisation.V2.PriorityScheduler")) .Returns(Create>().Object); var objectSaverMock = Create(); - objectSaverMock.Setup(x => x.Dispose()); var baseChildFinderMock = Create(); var baseSerializerMock = Create(); using var cts = new CancellationTokenSource();