Compare commits

..

14 Commits

Author SHA1 Message Date
Adam Hathcock c0a9291632 Merge pull request #344 from specklesystems/oguzhan/level-proxies
.NET Build and Publish / build (push) Has been cancelled
Feat(objects): level proxies
2025-06-23 15:05:15 +01:00
oguzhankoral b783d2acb6 Format 2025-06-23 15:57:24 +03:00
oguzhankoral 93539adc1e Add level proxies 2025-06-23 15:42:48 +03:00
Adam Hathcock 50906b172a Merge pull request #340 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
2025-06-11 17:35:27 +01:00
Adam Hathcock 05f7353925 Revert "Merge pull request #335 from specklesystems/adam/cnx-1786-allow-multiple-sends-to-access-sqlite-in-a-non-locking-2" (#339)
This reverts commit 59019bf846, reversing
changes made to 3afaf61a1a.

Co-authored-by: Adam Hathcock <adam@Adams-Mac-mini.localdomain>
2025-06-11 15:32:06 +00:00
Adam Hathcock 8328498553 Merge pull request #338 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
Dev to Main (no squash)
2025-06-11 11:07:20 +01:00
Adam Hathcock 59019bf846 Merge pull request #335 from specklesystems/adam/cnx-1786-allow-multiple-sends-to-access-sqlite-in-a-non-locking-2
Use an object saver per stream instead of sqlite manager per stream
2025-06-11 10:50:34 +01:00
Adam Hathcock 3afaf61a1a Merge pull request #337 from specklesystems/main
Main to dev (no squash)
2025-06-11 10:50:18 +01:00
Adam Hathcock 424609fad0 fix tests 2025-06-10 13:18:34 +01:00
Adam Hathcock 46c067308e Fix DI dependency and tests 2025-06-10 11:39:42 +01:00
Adam Hathcock 0e33e8df8f add tests 2025-06-10 11:33:17 +01:00
Adam Hathcock bc81c21e9d format 2025-06-10 11:15:30 +01:00
Adam Hathcock 7f8b59d348 Pool object savers instead of sqlite 2025-06-10 11:15:01 +01:00
Adam Hathcock 44ba61e4a5 Adjustments to avoid sqlite "database is locked" errors (#333)
* add new exception test

* Make memory tests and file path tests be explicit

* set the default write parallelism to 1

* set to single reader for caching channel

* format

* Try to have consistent DB locked error test

* always a single reader of the channel

* Remove extra snapshot

* Revert "Try to have consistent DB locked error test"

This reverts commit 93669c57a3.

* remove extra test that doesn't do anything
2025-06-09 16:24:39 +00:00
12 changed files with 136 additions and 98 deletions
+20
View File
@@ -0,0 +1,20 @@
using Speckle.Objects.Data;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Proxies;
namespace Speckle.Objects.Other;
/// <summary>
/// Proxy for levels as DataObject value.
/// <remarks> These proxy lives in Objects library because it depends on DataObject</remarks>
/// </summary>
[SpeckleType("Objects.Other.LevelProxy")]
public class LevelProxy : Base, IProxyCollection
{
/// <summary>
/// The list of application ids of objects that use this level
/// </summary>
public required List<string> objects { get; set; }
public required DataObject value { get; set; }
}
@@ -1,7 +1,6 @@
using System.Drawing;
using Speckle.Newtonsoft.Json;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Proxies;
namespace Speckle.Objects.Other;
@@ -39,20 +38,3 @@ public class RenderMaterial : Base
set => diffuse = value.ToArgb();
}
}
/// <summary>
/// Used to store render material to object relationships in root collections
/// </summary>
[SpeckleType("Objects.Other.RenderMaterialProxy")]
public class RenderMaterialProxy : Base, IProxyCollection
{
/// <summary>
/// The list of application ids of objects that use this render material
/// </summary>
public required List<string> objects { get; set; }
/// <summary>
/// The render material used by <see cref="objects"/>
/// </summary>
public required RenderMaterial value { get; set; }
}
@@ -0,0 +1,22 @@
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Proxies;
namespace Speckle.Objects.Other;
/// <summary>
/// Used to store render material to object relationships in root collections
/// <remarks> These proxy lives in Objects library because it depends on RenderMaterial</remarks>
/// </summary>
[SpeckleType("Objects.Other.RenderMaterialProxy")]
public class RenderMaterialProxy : Base, IProxyCollection
{
/// <summary>
/// The list of application ids of objects that use this render material
/// </summary>
public required List<string> objects { get; set; }
/// <summary>
/// The render material used by <see cref="objects"/>
/// </summary>
public required RenderMaterial value { get; set; }
}
@@ -13,8 +13,8 @@ public abstract class ChannelSaver<T>
private static readonly TimeSpan HTTP_BATCH_TIMEOUT = TimeSpan.FromSeconds(2);
private const int MAX_PARALLELISM_HTTP = 4;
private const int HTTP_CAPACITY = 500;
private const int MAX_CACHE_WRITE_PARALLELISM = 4;
private const int MAX_CACHE_BATCH = 500;
private const int MAX_CACHE_WRITE_PARALLELISM = 1;
private const int MAX_CACHE_BATCH = 1000;
private readonly Channel<T> _checkCacheChannel = Channel.CreateBounded<T>(
new BoundedChannelOptions(SEND_CAPACITY)
@@ -45,9 +45,9 @@ public abstract class ChannelSaver<T>
cancellationToken
)
.Join()
.Batch(cacheBatchSize ?? MAX_CACHE_BATCH)
.Batch(cacheBatchSize ?? MAX_CACHE_BATCH, singleReader: true)
.WithTimeout(HTTP_BATCH_TIMEOUT)
.ReadAllConcurrently(maxParallelism ?? MAX_CACHE_WRITE_PARALLELISM, SaveToCache, cancellationToken)
.ReadAllConcurrently(MAX_CACHE_WRITE_PARALLELISM, SaveToCache, cancellationToken)
.ContinueWith(
t =>
{
@@ -13,7 +13,26 @@ public sealed class SqLiteJsonCacheManager : ISqLiteJsonCacheManager
{
private readonly CacheDbCommandPool _pool;
public SqLiteJsonCacheManager(string path, int concurrency)
public static ISqLiteJsonCacheManager FromMemory(int concurrency) => new SqLiteJsonCacheManager(concurrency);
private SqLiteJsonCacheManager(int concurrency)
{
//disable pooling as we pool ourselves
var builder = new SqliteConnectionStringBuilder
{
Pooling = false,
DataSource = ":memory:",
Cache = SqliteCacheMode.Shared,
Mode = SqliteOpenMode.Memory,
};
_pool = new CacheDbCommandPool(builder.ToString(), concurrency);
Initialize();
}
public static ISqLiteJsonCacheManager FromFilePath(string path, int concurrency) =>
new SqLiteJsonCacheManager(path, concurrency);
private SqLiteJsonCacheManager(string path, int concurrency)
{
//disable pooling as we pool ourselves
var builder = new SqliteConnectionStringBuilder { Pooling = false, DataSource = path };
@@ -47,12 +66,6 @@ public sealed class SqLiteJsonCacheManager : ISqLiteJsonCacheManager
command.ExecuteNonQuery();
}
// Insert Optimisations
//Note / Hack: This setting has the potential to corrupt the db.
//cmd = new SqliteCommand("PRAGMA synchronous=OFF;", Connection);
//cmd.ExecuteNonQuery();
using (SqliteCommand cmd1 = new("PRAGMA count_changes=OFF;", c))
{
cmd1.ExecuteNonQuery();
@@ -9,7 +9,8 @@ public class SqLiteJsonCacheManagerFactory : ISqLiteJsonCacheManagerFactory
{
public const int INITIAL_CONCURRENCY = 4;
private ISqLiteJsonCacheManager Create(string path, int concurrency) => new SqLiteJsonCacheManager(path, concurrency);
private ISqLiteJsonCacheManager Create(string path, int concurrency) =>
SqLiteJsonCacheManager.FromFilePath(path, concurrency);
public ISqLiteJsonCacheManager CreateForUser(string scope) =>
Create(Path.Combine(SpecklePathProvider.UserApplicationDataPath(), "Speckle", $"{scope}.db"), 1);
@@ -16,8 +16,8 @@ public record SerializeProcessOptions(
bool SkipFindTotalObjects = false
)
{
public int? MaxHttpSendSize { get; set; }
public int? MaxCacheSize { get; set; }
public int? MaxHttpSendBatchSize { get; set; }
public int? MaxCacheBatchSize { get; set; }
public int? MaxParallelism { get; set; }
}
@@ -112,8 +112,8 @@ public sealed class SerializeProcess(
{
var channelTask = objectSaver.Start(
options?.MaxParallelism,
options?.MaxHttpSendSize,
options?.MaxCacheSize,
options?.MaxHttpSendBatchSize,
options?.MaxCacheBatchSize,
_processSource.Token
);
var findTotalObjectsTask = Task.CompletedTask;
@@ -64,18 +64,10 @@ public class SerializeProcessFactory(
#pragma warning disable CA2000
var memoryJsonCacheManager = new MemoryJsonCacheManager(jsonCache);
#pragma warning restore CA2000
return new SerializeProcess(
return CreateSerializeProcess(
memoryJsonCacheManager,
new MemoryServerObjectManager(objects),
progress,
new ObjectSaver(
progress,
memoryJsonCacheManager,
new MemoryServerObjectManager(objects),
loggerFactory.CreateLogger<ObjectSaver>(),
cancellationToken
),
baseChildFinder,
new BaseSerializer(memoryJsonCacheManager, objectSerializerFactory),
loggerFactory,
cancellationToken,
options
);
@@ -123,7 +123,7 @@ public class DetachedTests
objects,
null,
default,
new SerializeProcessOptions(false, false, true, true) { MaxParallelism = 1, MaxHttpSendSize = 1 }
new SerializeProcessOptions(false, false, true, 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, MaxHttpSendSize = 1 }
new SerializeProcessOptions(false, false, true, 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, MaxHttpSendSize = 1 }
new SerializeProcessOptions(false, false, true, true) { MaxParallelism = 1, MaxHttpSendBatchSize = 1 }
);
var results = await serializeProcess.Serialize(@base);
@@ -95,8 +95,8 @@ public class ExceptionTests
default,
new SerializeProcessOptions(false, false, false, true)
{
MaxHttpSendSize = 1,
MaxCacheSize = 1,
MaxHttpSendBatchSize = 1,
MaxCacheBatchSize = 1,
MaxParallelism = 1,
}
);
@@ -14,6 +14,7 @@ using Speckle.Sdk.Serialisation.V2;
using Speckle.Sdk.Serialisation.V2.Receive;
using Speckle.Sdk.Serialisation.V2.Send;
using Speckle.Sdk.Serialization.Tests.Framework;
using Speckle.Sdk.SQLite;
using Speckle.Sdk.Testing.Framework;
namespace Speckle.Sdk.Serialization.Tests;
@@ -50,45 +51,45 @@ public class SerializationTests
public void Dispose() { }
}
[Theory]
[InlineData("RevitObject.json.gz")]
public async Task Basic_Namespace_Validation(string fileName)
{
var closures = TestFileManager.GetFileAsClosures(fileName);
var deserializer = new SpeckleObjectDeserializer
/* [Theory]
[InlineData("RevitObject.json.gz")]
public async Task Basic_Namespace_Validation(string fileName)
{
ReadTransport = new TestTransport(closures),
CancellationToken = default,
};
foreach (var (id, objJson) in closures)
{
var jObject = JObject.Parse(objJson);
var oldSpeckleType = jObject["speckle_type"].NotNull().Value<string>().NotNull();
var starts = oldSpeckleType.StartsWith("Speckle.Core.") || oldSpeckleType.StartsWith("Objects.");
starts.Should().BeTrue($"{oldSpeckleType} isn't expected");
var baseType = await deserializer.DeserializeAsync(objJson);
baseType.id.Should().Be(id);
var oldType = TypeLoader.GetAtomicType(oldSpeckleType);
if (oldType == typeof(Base))
var closures = TestFileManager.GetFileAsClosures(fileName);
var deserializer = new SpeckleObjectDeserializer
{
oldSpeckleType.Should().NotContain("Base");
}
else
ReadTransport = new TestTransport(closures),
CancellationToken = default,
};
foreach (var (id, objJson) in closures)
{
starts = baseType.speckle_type.StartsWith("Speckle.Core.") || baseType.speckle_type.StartsWith("Objects.");
starts.Should().BeTrue($"{baseType.speckle_type} isn't expected");
var type = TypeLoader.GetAtomicType(baseType.speckle_type);
type.Should().NotBeNull();
var name = TypeLoader.GetTypeString(type) ?? throw new ArgumentNullException($"Could not find: {type}");
starts = name.StartsWith("Speckle.Core") || name.StartsWith("Objects");
starts.Should().BeTrue($"{name} isn't expected");
var jObject = JObject.Parse(objJson);
var oldSpeckleType = jObject["speckle_type"].NotNull().Value<string>().NotNull();
var starts = oldSpeckleType.StartsWith("Speckle.Core.") || oldSpeckleType.StartsWith("Objects.");
starts.Should().BeTrue($"{oldSpeckleType} isn't expected");
var baseType = await deserializer.DeserializeAsync(objJson);
baseType.id.Should().Be(id);
var oldType = TypeLoader.GetAtomicType(oldSpeckleType);
if (oldType == typeof(Base))
{
oldSpeckleType.Should().NotContain("Base");
}
else
{
starts = baseType.speckle_type.StartsWith("Speckle.Core.") || baseType.speckle_type.StartsWith("Objects.");
starts.Should().BeTrue($"{baseType.speckle_type} isn't expected");
var type = TypeLoader.GetAtomicType(baseType.speckle_type);
type.Should().NotBeNull();
var name = TypeLoader.GetTypeString(type) ?? throw new ArgumentNullException($"Could not find: {type}");
starts = name.StartsWith("Speckle.Core") || name.StartsWith("Objects");
starts.Should().BeTrue($"{name} isn't expected");
}
}
}
}
}*/
[Theory]
[InlineData("RevitObject.json.gz")]
@@ -184,9 +185,16 @@ public class SerializationTests
}
[Theory]
[InlineData("RevitObject.json.gz", "3416d3fe01c9196115514c4a2f41617b", 7818, 4674)]
public async Task Roundtrip_Test_New(string fileName, string rootId, int oldCount, int newCount)
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
public async Task Roundtrip_Test_New(int concurrency)
{
string fileName = "RevitObject.json.gz";
string rootId = "3416d3fe01c9196115514c4a2f41617b";
int oldCount = 7818;
int newCount = 4674;
var closures = TestFileManager.GetFileAsClosures(fileName);
closures.Count.Should().Be(oldCount);
@@ -218,11 +226,11 @@ public class SerializationTests
await using (
var serializeProcess = _factory.CreateSerializeProcess(
new ConcurrentDictionary<Id, Json>(),
newIdToJson,
SqLiteJsonCacheManager.FromMemory(1),
new MemoryServerObjectManager(newIdToJson),
null,
default,
new SerializeProcessOptions(true, true, false, true)
new SerializeProcessOptions(false, false, false, true) { MaxCacheBatchSize = 1, MaxParallelism = concurrency }
)
)
{
@@ -22,7 +22,7 @@ public class SQLiteJsonCacheManagerTests : IDisposable
public void TestGetAll()
{
var data = new List<(string id, string json)>() { ("id1", "1"), ("id2", "2") };
using var manager = new SqLiteJsonCacheManager(_basePath, 2);
using var manager = SqLiteJsonCacheManager.FromFilePath(_basePath, 2);
manager.SaveObjects(data);
var items = manager.GetAllObjects();
items.Count.Should().Be(data.Count);
@@ -38,7 +38,7 @@ public class SQLiteJsonCacheManagerTests : IDisposable
public void TestGet()
{
var data = new List<(string id, string json)>() { ("id1", "1"), ("id2", "2") };
using var manager = new SqLiteJsonCacheManager(_basePath, 2);
using var manager = SqLiteJsonCacheManager.FromFilePath(_basePath, 2);
foreach (var d in data)
{
manager.SaveObject(d.id, d.json);
@@ -84,7 +84,7 @@ public class SQLiteJsonCacheManagerTests : IDisposable
public void TestLargeJsonPayload()
{
var largeJson = new string('a', 100_000);
using var manager = new SqLiteJsonCacheManager(_basePath, 2);
using var manager = SqLiteJsonCacheManager.FromFilePath(_basePath, 2);
manager.SaveObject("large", largeJson);
var result = manager.GetObject("large");
result.Should().Be(largeJson);
@@ -96,7 +96,7 @@ public class SQLiteJsonCacheManagerTests : IDisposable
var id = "spécial_字符_!@#$%^&*()";
var json = /*lang=json,strict*/
"{\"value\": \"特殊字符!@#$%^&*()\"}";
using var manager = new SqLiteJsonCacheManager(_basePath, 2);
using var manager = SqLiteJsonCacheManager.FromFilePath(_basePath, 2);
manager.SaveObject(id, json);
var result = manager.GetObject(id);
result.Should().Be(json);
@@ -108,7 +108,7 @@ public class SQLiteJsonCacheManagerTests : IDisposable
[Fact]
public void TestBulkInsertEmptyCollection()
{
using var manager = new SqLiteJsonCacheManager(_basePath, 2);
using var manager = SqLiteJsonCacheManager.FromFilePath(_basePath, 2);
manager.SaveObjects(new List<(string, string)>());
manager.GetAllObjects().Count.Should().Be(0);
}
@@ -116,7 +116,7 @@ public class SQLiteJsonCacheManagerTests : IDisposable
[Fact]
public void TestRepeatedUpdateAndDelete()
{
using var manager = new SqLiteJsonCacheManager(_basePath, 2);
using var manager = SqLiteJsonCacheManager.FromFilePath(_basePath, 2);
manager.SaveObject("id", "1");
manager.UpdateObject("id", "2");
manager.UpdateObject("id", "3");
@@ -129,7 +129,7 @@ public class SQLiteJsonCacheManagerTests : IDisposable
[Fact]
public void TestGetAndDeleteNonExistentId()
{
using var manager = new SqLiteJsonCacheManager(_basePath, 2);
using var manager = SqLiteJsonCacheManager.FromFilePath(_basePath, 2);
manager.GetObject("doesnotexist").Should().BeNull();
manager.HasObject("doesnotexist").Should().BeFalse();
manager.DeleteObject("doesnotexist"); // Should not throw
@@ -138,7 +138,7 @@ public class SQLiteJsonCacheManagerTests : IDisposable
[Fact]
public void TestNullOrEmptyInput()
{
using var manager = new SqLiteJsonCacheManager(_basePath, 2);
using var manager = SqLiteJsonCacheManager.FromFilePath(_basePath, 2);
// Empty id
Assert.Throws<ArgumentException>(() => manager.SaveObject("", "emptyid"));
// Empty json