From af5ce194f2dad67ad533068a53d3727abe949ac0 Mon Sep 17 00:00:00 2001 From: Benjamin Petit Date: Thu, 17 Nov 2022 21:03:22 +0100 Subject: [PATCH 1/5] Utilities to serialize some type in a 7.0 compliant way --- Orleans.sln | 30 ++ global.json | 2 +- .../Properties/AssemblyInfo.cs | 2 + src/Orleans.Core/Properties/AssemblyInfo.cs | 2 + .../GrainReferenceExtractor.cs | 117 +++++++ .../HostingExtensions.cs | 29 ++ .../Import/GenericGrainInterfaceType.cs | 76 +++++ .../Import/GenericGrainType.cs | 80 +++++ .../Import/GrainIdKeyExtensions.cs | 162 ++++++++++ .../Import/GrainInterfaceType.cs | 170 ++++++++++ .../Import/GrainInterfaceTypeResolver.cs | 90 ++++++ .../Import/GrainType.cs | 132 ++++++++ .../Import/GrainTypeResolver.cs | 104 +++++++ .../Import/IGrainTypeProvider.cs | 100 ++++++ .../Import/IdSpan.cs | 98 ++++++ .../Import/TypeConverterExtensions.cs | 116 +++++++ .../Orleans.Persistence.Migration.csproj | 20 ++ .../Serialization/OrleansJsonSerializer.cs | 291 ++++++++++++++++++ .../OrleansJsonSerializerOptions.cs | 31 ++ .../OrleansJsonSerializerSettings.cs | 86 ++++++ .../Properties/AssemblyInfo.cs | 2 + .../Migration.Tests/CsvDataReader.cs | 112 +++++++ .../GrainReferenceExtractorTests.cs | 114 +++++++ test/Extensions/Migration.Tests/Grains.cs | 78 +++++ .../Migration.Tests/JsonMigrationTests.cs | 44 +++ .../Migration.Tests/Migration.Tests.csproj | 35 +++ .../MigrationDefaultClusterFixture.cs | 48 +++ .../Migration.Tests/TestExtensions.cs | 46 +++ .../Migration.Tests/grain-references.csv | 223 ++++++++++++++ .../TestExtensions/DefaultClusterFixture.cs | 4 +- 30 files changed, 2441 insertions(+), 3 deletions(-) create mode 100644 src/Orleans.Persistence.Migration/GrainReferenceExtractor.cs create mode 100644 src/Orleans.Persistence.Migration/HostingExtensions.cs create mode 100644 src/Orleans.Persistence.Migration/Import/GenericGrainInterfaceType.cs create mode 100644 src/Orleans.Persistence.Migration/Import/GenericGrainType.cs create mode 100644 src/Orleans.Persistence.Migration/Import/GrainIdKeyExtensions.cs create mode 100644 src/Orleans.Persistence.Migration/Import/GrainInterfaceType.cs create mode 100644 src/Orleans.Persistence.Migration/Import/GrainInterfaceTypeResolver.cs create mode 100644 src/Orleans.Persistence.Migration/Import/GrainType.cs create mode 100644 src/Orleans.Persistence.Migration/Import/GrainTypeResolver.cs create mode 100644 src/Orleans.Persistence.Migration/Import/IGrainTypeProvider.cs create mode 100644 src/Orleans.Persistence.Migration/Import/IdSpan.cs create mode 100644 src/Orleans.Persistence.Migration/Import/TypeConverterExtensions.cs create mode 100644 src/Orleans.Persistence.Migration/Orleans.Persistence.Migration.csproj create mode 100644 src/Orleans.Persistence.Migration/Serialization/OrleansJsonSerializer.cs create mode 100644 src/Orleans.Persistence.Migration/Serialization/OrleansJsonSerializerOptions.cs create mode 100644 src/Orleans.Persistence.Migration/Serialization/OrleansJsonSerializerSettings.cs create mode 100644 test/Extensions/Migration.Tests/CsvDataReader.cs create mode 100644 test/Extensions/Migration.Tests/GrainReferenceExtractorTests.cs create mode 100644 test/Extensions/Migration.Tests/Grains.cs create mode 100644 test/Extensions/Migration.Tests/JsonMigrationTests.cs create mode 100644 test/Extensions/Migration.Tests/Migration.Tests.csproj create mode 100644 test/Extensions/Migration.Tests/MigrationDefaultClusterFixture.cs create mode 100644 test/Extensions/Migration.Tests/TestExtensions.cs create mode 100644 test/Extensions/Migration.Tests/grain-references.csv diff --git a/Orleans.sln b/Orleans.sln index 5a709210ee..630638a0ac 100644 --- a/Orleans.sln +++ b/Orleans.sln @@ -216,6 +216,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DistributedTests.Client", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DistributedTests.Server", "test\DistributedTests\DistributedTests.Server\DistributedTests.Server.csproj", "{E8335DC9-9A7F-45C1-AFA3-0AA93ABD4FA5}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Orleans.Persistence.Migration", "src\Orleans.Persistence.Migration\Orleans.Persistence.Migration.csproj", "{E0B94181-200A-4CC6-94CA-D3D81CA6720A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Migration.Tests", "test\Extensions\Migration.Tests\Migration.Tests.csproj", "{57285DA6-6EF1-4B46-A178-1ABB48CD0B57}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1318,6 +1322,30 @@ Global {E8335DC9-9A7F-45C1-AFA3-0AA93ABD4FA5}.Release|x64.Build.0 = Release|Any CPU {E8335DC9-9A7F-45C1-AFA3-0AA93ABD4FA5}.Release|x86.ActiveCfg = Release|Any CPU {E8335DC9-9A7F-45C1-AFA3-0AA93ABD4FA5}.Release|x86.Build.0 = Release|Any CPU + {E0B94181-200A-4CC6-94CA-D3D81CA6720A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E0B94181-200A-4CC6-94CA-D3D81CA6720A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0B94181-200A-4CC6-94CA-D3D81CA6720A}.Debug|x64.ActiveCfg = Debug|Any CPU + {E0B94181-200A-4CC6-94CA-D3D81CA6720A}.Debug|x64.Build.0 = Debug|Any CPU + {E0B94181-200A-4CC6-94CA-D3D81CA6720A}.Debug|x86.ActiveCfg = Debug|Any CPU + {E0B94181-200A-4CC6-94CA-D3D81CA6720A}.Debug|x86.Build.0 = Debug|Any CPU + {E0B94181-200A-4CC6-94CA-D3D81CA6720A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E0B94181-200A-4CC6-94CA-D3D81CA6720A}.Release|Any CPU.Build.0 = Release|Any CPU + {E0B94181-200A-4CC6-94CA-D3D81CA6720A}.Release|x64.ActiveCfg = Release|Any CPU + {E0B94181-200A-4CC6-94CA-D3D81CA6720A}.Release|x64.Build.0 = Release|Any CPU + {E0B94181-200A-4CC6-94CA-D3D81CA6720A}.Release|x86.ActiveCfg = Release|Any CPU + {E0B94181-200A-4CC6-94CA-D3D81CA6720A}.Release|x86.Build.0 = Release|Any CPU + {57285DA6-6EF1-4B46-A178-1ABB48CD0B57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {57285DA6-6EF1-4B46-A178-1ABB48CD0B57}.Debug|Any CPU.Build.0 = Debug|Any CPU + {57285DA6-6EF1-4B46-A178-1ABB48CD0B57}.Debug|x64.ActiveCfg = Debug|Any CPU + {57285DA6-6EF1-4B46-A178-1ABB48CD0B57}.Debug|x64.Build.0 = Debug|Any CPU + {57285DA6-6EF1-4B46-A178-1ABB48CD0B57}.Debug|x86.ActiveCfg = Debug|Any CPU + {57285DA6-6EF1-4B46-A178-1ABB48CD0B57}.Debug|x86.Build.0 = Debug|Any CPU + {57285DA6-6EF1-4B46-A178-1ABB48CD0B57}.Release|Any CPU.ActiveCfg = Release|Any CPU + {57285DA6-6EF1-4B46-A178-1ABB48CD0B57}.Release|Any CPU.Build.0 = Release|Any CPU + {57285DA6-6EF1-4B46-A178-1ABB48CD0B57}.Release|x64.ActiveCfg = Release|Any CPU + {57285DA6-6EF1-4B46-A178-1ABB48CD0B57}.Release|x64.Build.0 = Release|Any CPU + {57285DA6-6EF1-4B46-A178-1ABB48CD0B57}.Release|x86.ActiveCfg = Release|Any CPU + {57285DA6-6EF1-4B46-A178-1ABB48CD0B57}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1427,6 +1455,8 @@ Global {7F6A75BB-72AE-4509-8E82-55C764D0C070} = {FFEC9FEE-FEDF-4510-B7D2-0B0B3374ED2F} {25D20278-8901-47CC-AD1D-F3C4BEB845BF} = {FFEC9FEE-FEDF-4510-B7D2-0B0B3374ED2F} {E8335DC9-9A7F-45C1-AFA3-0AA93ABD4FA5} = {FFEC9FEE-FEDF-4510-B7D2-0B0B3374ED2F} + {E0B94181-200A-4CC6-94CA-D3D81CA6720A} = {FE2E08C6-9C3B-4AEE-AE07-CCA387580D7A} + {57285DA6-6EF1-4B46-A178-1ABB48CD0B57} = {082D25DB-70CA-48F4-93E0-EC3455F494B8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7BFB3429-B5BB-4DB1-95B4-67D77A864952} diff --git a/global.json b/global.json index 5e8bf6cbc4..98052da7b7 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "feature", - "version": "6.0.400" + "version": "7.0.100" } } diff --git a/src/Orleans.Core.Abstractions/Properties/AssemblyInfo.cs b/src/Orleans.Core.Abstractions/Properties/AssemblyInfo.cs index 45c834f350..13fa2d9c16 100644 --- a/src/Orleans.Core.Abstractions/Properties/AssemblyInfo.cs +++ b/src/Orleans.Core.Abstractions/Properties/AssemblyInfo.cs @@ -13,5 +13,7 @@ [assembly: InternalsVisibleTo("TestInternalGrainInterfaces")] [assembly: InternalsVisibleTo("TestInternalGrains")] +[assembly: InternalsVisibleTo("Orleans.Persistence.Migration")] + // Mocking libraries [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file diff --git a/src/Orleans.Core/Properties/AssemblyInfo.cs b/src/Orleans.Core/Properties/AssemblyInfo.cs index 8882946928..3fdef7f179 100644 --- a/src/Orleans.Core/Properties/AssemblyInfo.cs +++ b/src/Orleans.Core/Properties/AssemblyInfo.cs @@ -26,6 +26,8 @@ [assembly: InternalsVisibleTo("TestInternalGrains")] [assembly: InternalsVisibleTo("CodeGenerator.Tests")] +[assembly: InternalsVisibleTo("Orleans.Persistence.Migration")] + [assembly: KnownAssembly(typeof(IGrain))] // Mocking libraries diff --git a/src/Orleans.Persistence.Migration/GrainReferenceExtractor.cs b/src/Orleans.Persistence.Migration/GrainReferenceExtractor.cs new file mode 100644 index 0000000000..573a753177 --- /dev/null +++ b/src/Orleans.Persistence.Migration/GrainReferenceExtractor.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Orleans.Runtime; +using Orleans.Serialization.Configuration; +using Orleans.Serialization; +using Orleans.Serialization.TypeSystem; +using Orleans.Core; +using System.Diagnostics.Metrics; +using Orleans.Metadata; +using GrainTypeResolver = Orleans.Metadata.GrainTypeResolver; + +namespace Orleans.Persistence.Migration +{ + public interface IGrainReferenceExtractor + { + (GrainType grainType, GrainInterfaceType grainInterfaceType, IdSpan key) Extract(GrainReference grainReference); + } + + public static class GrainReferenceExtractorExtension + { + public static string GetGrainId(this IGrainReferenceExtractor @this, GrainReference grainReference) + { + var (type, _, key) = @this.Extract(grainReference); + return $"{type}/{key}"; + } + } + + internal class GrainReferenceExtractor : IGrainReferenceExtractor + { + private readonly GrainTypeManager _grainTypeManager; + private readonly GrainTypeResolver _grainTypeResolver; + private readonly GrainInterfaceTypeResolver _grainInterfaceTypeResolver; + + public GrainReferenceExtractor( + GrainTypeManager grainTypeManager, + GrainTypeResolver grainTypeResolver, + GrainInterfaceTypeResolver grainInterfaceTypeResolver) + { + _grainTypeManager = grainTypeManager; + _grainTypeResolver = grainTypeResolver; + _grainInterfaceTypeResolver = grainInterfaceTypeResolver; + } + + public (GrainType grainType, GrainInterfaceType grainInterfaceType, IdSpan key) Extract(GrainReference grainReference) + { + // Get GrainType + var typeCode = grainReference.GrainIdentity.TypeCode; + _grainTypeManager.GetTypeInfo(typeCode, out var grainClass, out _, grainReference.GenericArguments); + Type grainType = LookupType(grainClass) ?? throw new ArgumentException("Grain type not found"); + var type = _grainTypeResolver.GetGrainType(grainType); + + // Get GrainInterfaceType + Type iface = null; + if (_grainTypeManager.GrainTypeResolver.TryGetInterfaceData(grainReference.InterfaceId, out var interfaceData)) + { + if (interfaceData.Interface.IsGenericType) + { + // We cannot use grainReference.InterfaceName because it doesn't match + foreach (var candidate in grainType.GetInterfaces()) + { + if (candidate.Name.Equals(interfaceData.Interface.Name, StringComparison.OrdinalIgnoreCase)) + { + iface = candidate; + break; + } + } + } + else + { + iface = interfaceData.Interface; + } + } + if (iface == null) + { + throw new ArgumentException("Grain interface type not found"); + } + var interfaceType = _grainInterfaceTypeResolver.GetGrainInterfaceType(iface); + + // Extract Key + IdSpan key; + if (grainReference.IsPrimaryKeyBasedOnLong()) + { + var keyBase = grainReference.GetPrimaryKeyLong(out var keyExt); + // Check if it is a string key + key = keyBase == 0 && grainReference.GetType().IsAssignableTo(typeof(IGrainWithStringKey)) + ? IdSpan.Create(keyExt) + : GrainIdKeyExtensions.CreateIntegerKey(keyBase, keyExt); + } + else + { + var keyBase = grainReference.GetPrimaryKey(out var keyExt); + key = GrainIdKeyExtensions.CreateGuidKey(keyBase, keyExt); + } + + return (type, interfaceType, key); + } + + private static Type LookupType(string typeName) + { + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + var grainType = assembly.GetType(typeName); + if (grainType != null) + { + return grainType; + } + } + return null; + } + } +} diff --git a/src/Orleans.Persistence.Migration/HostingExtensions.cs b/src/Orleans.Persistence.Migration/HostingExtensions.cs new file mode 100644 index 0000000000..40780353a9 --- /dev/null +++ b/src/Orleans.Persistence.Migration/HostingExtensions.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Orleans.Hosting; +using Orleans.Metadata; +using Orleans.Persistence.Migration.Serialization; +using Orleans.Serialization.TypeSystem; + +namespace Orleans.Persistence.Migration +{ + public static class HostingExtensions + { + public static ISiloBuilder AddMigrationTools(this ISiloBuilder builder) + { + builder.ConfigureServices(services => + { + services + .AddSingleton, ConfigureOrleansJsonSerializerOptions>() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + }); + return builder; + } + } +} diff --git a/src/Orleans.Persistence.Migration/Import/GenericGrainInterfaceType.cs b/src/Orleans.Persistence.Migration/Import/GenericGrainInterfaceType.cs new file mode 100644 index 0000000000..313a06d907 --- /dev/null +++ b/src/Orleans.Persistence.Migration/Import/GenericGrainInterfaceType.cs @@ -0,0 +1,76 @@ +using System; +using System.Buffers.Text; +using Orleans.Serialization.TypeSystem; +using Orleans.Utilities; + +namespace Orleans.Runtime +{ + /// + /// Represents a that is parameterized using type parameters. + /// + [Immutable] + public readonly struct GenericGrainInterfaceType + { + /// + /// Initializes a new instance of the struct. + /// + /// The underlying grain interface type. + private GenericGrainInterfaceType(GrainInterfaceType value) + { + Value = value; + } + + /// + /// The underlying + /// + public GrainInterfaceType Value { get; } + + /// + /// Returns if this instance contains concrete type parameters. + /// + public bool IsConstructed => TypeConverterExtensions.IsConstructed(this.Value.Value); + + /// + /// Returns the generic interface id corresponding to the provided value. + /// + public static bool TryParse(GrainInterfaceType grainType, out GenericGrainInterfaceType result) + { + if (!grainType.IsDefault && TypeConverterExtensions.IsGenericType(grainType.Value)) + { + result = new GenericGrainInterfaceType(grainType); + return true; + } + + result = default; + return false; + } + + /// + /// Returns a non-constructed version of this instance. + /// + public GenericGrainInterfaceType GetGenericGrainType() + { + var generic = TypeConverterExtensions.GetDeconstructed(Value.Value); + return new GenericGrainInterfaceType(new GrainInterfaceType(generic)); + } + + /// + /// Returns a constructed version of this instance. + /// + public GenericGrainInterfaceType Construct(TypeConverter formatter, params Type[] typeArguments) + { + var constructed = formatter.GetConstructed(this.Value.Value, typeArguments); + return new GenericGrainInterfaceType(new GrainInterfaceType(constructed)); + } + + /// + /// Returns the type arguments which this instance was constructed with. + /// + public Type[] GetArguments(TypeConverter formatter) => formatter.GetArguments(this.Value.Value); + + /// + /// Returns a UTF8 interpretation of the current instance. + /// + public override string ToString() => Value.ToString(); + } +} diff --git a/src/Orleans.Persistence.Migration/Import/GenericGrainType.cs b/src/Orleans.Persistence.Migration/Import/GenericGrainType.cs new file mode 100644 index 0000000000..e0ed98fc08 --- /dev/null +++ b/src/Orleans.Persistence.Migration/Import/GenericGrainType.cs @@ -0,0 +1,80 @@ +using System; +using Orleans.Serialization.TypeSystem; +using Orleans.Utilities; + +namespace Orleans.Runtime +{ + /// + /// Represents a that is parameterized using type parameters. + /// + public readonly struct GenericGrainType : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The underlying grain type. + private GenericGrainType(GrainType grainType) + { + GrainType = grainType; + } + + /// + /// The underlying grain type. + /// + public GrainType GrainType { get; } + + /// + /// Returns if this instance contains concrete type parameters. + /// + public bool IsConstructed => TypeConverterExtensions.IsConstructed(this.GrainType.Value); + + /// + /// Returns the generic grain type corresponding to the provided value. + /// + public static bool TryParse(GrainType grainType, out GenericGrainType result) + { + if (TypeConverterExtensions.IsGenericType(grainType.Value)) + { + result = new GenericGrainType(grainType); + return true; + } + + result = default; + return false; + } + + /// + /// Returns a non-constructed version of this instance. + /// + public GenericGrainType GetUnconstructedGrainType() + { + var generic = TypeConverterExtensions.GetDeconstructed(GrainType.Value); + return new GenericGrainType(new GrainType(generic)); + } + + /// + /// Returns a constructed version of this instance. + /// + public GenericGrainType Construct(TypeConverter formatter, params Type[] typeArguments) + { + var constructed = formatter.GetConstructed(this.GrainType.Value, typeArguments); + return new GenericGrainType(new GrainType(constructed)); + } + /// + /// Returns the type arguments which this instance was constructed with. + /// + public Type[] GetArguments(TypeConverter formatter) => formatter.GetArguments(this.GrainType.Value); + + /// + public override string ToString() => this.GrainType.ToString() ?? string.Empty; + + /// + public bool Equals(GenericGrainType other) => this.GrainType.Equals(other.GrainType); + + /// + public override bool Equals(object obj) => obj is GenericGrainType other && this.Equals(other); + + /// + public override int GetHashCode() => this.GrainType.GetHashCode(); + } +} diff --git a/src/Orleans.Persistence.Migration/Import/GrainIdKeyExtensions.cs b/src/Orleans.Persistence.Migration/Import/GrainIdKeyExtensions.cs new file mode 100644 index 0000000000..1d30e03993 --- /dev/null +++ b/src/Orleans.Persistence.Migration/Import/GrainIdKeyExtensions.cs @@ -0,0 +1,162 @@ +using System; +using System.Buffers.Text; +using System.Diagnostics; +using System.Text; + +#nullable enable +namespace Orleans.Runtime +{ + /// + /// Extensions for keys. + /// + public static class GrainIdKeyExtensions + { + /// + /// Creates an representing a key. + /// + /// + /// The key. + /// + /// + /// An representing the provided key. + /// + public static IdSpan CreateIntegerKey(long key) + { + Span buf = stackalloc byte[sizeof(long) * 2]; + Utf8Formatter.TryFormat(key, buf, out var len, 'X'); + Debug.Assert(len > 0, "Unable to format the provided value as a UTF8 string"); + return new IdSpan(buf.Slice(0, len).ToArray()); + } + + /// + /// Creates an representing a key and key extension string. + /// + /// + /// The key. + /// + /// + /// The UTF-8 encoded key extension. + /// + /// + /// An representing the provided key and key extension. + /// + public static IdSpan CreateIntegerKey(long key, ReadOnlySpan keyExtension) + { + if (keyExtension.IsEmpty) + return CreateIntegerKey(key); + + Span tmp = stackalloc byte[sizeof(long) * 2]; + Utf8Formatter.TryFormat(key, tmp, out var len, 'X'); + Debug.Assert(len > 0, "Unable to format the provided value as a UTF8 string"); + + var buf = new byte[len + 1 + keyExtension.Length]; + tmp[..len].CopyTo(buf); + buf[len] = (byte)'+'; + keyExtension.CopyTo(buf.AsSpan(len + 1)); + return new(buf); + } + + /// + /// Creates an representing a key and key extension string. + /// + /// + /// The key. + /// + /// + /// The key extension. + /// + /// + /// An representing the provided key and key extension. + /// + public static IdSpan CreateIntegerKey(long key, string? keyExtension) + { + if (string.IsNullOrWhiteSpace(keyExtension)) + { + return CreateIntegerKey(key); + } + + Span tmp = stackalloc byte[sizeof(long) * 2]; + Utf8Formatter.TryFormat(key, tmp, out var len, 'X'); + Debug.Assert(len > 0, "Unable to format the provided value as a UTF8 string"); + + var extLen = Encoding.UTF8.GetByteCount(keyExtension); + var buf = new byte[len + 1 + extLen]; + tmp.Slice(0, len).CopyTo(buf); + buf[len] = (byte)'+'; + Encoding.UTF8.GetBytes(keyExtension, 0, keyExtension.Length, buf, len + 1); + + return new IdSpan(buf); + } + + /// + /// Creates an representing a key. + /// + /// + /// The key. + /// + /// + /// An representing the provided key. + /// + public static IdSpan CreateGuidKey(Guid key) + { + var buf = new byte[32]; + Utf8Formatter.TryFormat(key, buf, out var len, 'N'); + Debug.Assert(len == 32, "Unable to format the provided value as a UTF8 string"); + return new IdSpan(buf); + } + + /// + /// Creates an representing a key and key extension string. + /// + /// + /// The key. + /// + /// + /// The UTF-8 encoded key extension. + /// + /// + /// An representing the provided key and key extension. + /// + public static IdSpan CreateGuidKey(Guid key, ReadOnlySpan keyExtension) + { + if (keyExtension.IsEmpty) + return CreateGuidKey(key); + + var buf = new byte[32 + 1 + keyExtension.Length]; + Utf8Formatter.TryFormat(key, buf, out var len, 'N'); + Debug.Assert(len == 32, "Unable to format the provided value as a UTF8 string"); + buf[32] = (byte)'+'; + keyExtension.CopyTo(buf.AsSpan(len + 1)); + return new(buf); + } + + /// + /// Creates an representing a key and key extension string. + /// + /// + /// The key. + /// + /// + /// The key extension. + /// + /// + /// An representing the provided key and key extension. + /// + public static IdSpan CreateGuidKey(Guid key, string? keyExtension) + { + if (string.IsNullOrWhiteSpace(keyExtension)) + { + return CreateGuidKey(key); + } + + var extLen = Encoding.UTF8.GetByteCount(keyExtension); + var buf = new byte[32 + 1 + extLen]; + Utf8Formatter.TryFormat(key, buf, out var len, 'N'); + Debug.Assert(len == 32, "Unable to format the provided value as a UTF8 string"); + buf[32] = (byte)'+'; + Encoding.UTF8.GetBytes(keyExtension, 0, keyExtension.Length, buf, 33); + + return new IdSpan(buf); + } + } +} \ No newline at end of file diff --git a/src/Orleans.Persistence.Migration/Import/GrainInterfaceType.cs b/src/Orleans.Persistence.Migration/Import/GrainInterfaceType.cs new file mode 100644 index 0000000000..e43cf2c4d0 --- /dev/null +++ b/src/Orleans.Persistence.Migration/Import/GrainInterfaceType.cs @@ -0,0 +1,170 @@ +using System; + +#nullable enable +namespace Orleans.Runtime +{ + /// + /// Uniquely identifies a grain interface. + /// + [Serializable, GenerateSerializer, Immutable] + public readonly struct GrainInterfaceType : IEquatable, ISpanFormattable + { + /// + /// The underlying value. + /// + [Id(0)] + private readonly IdSpan _value; + + /// + /// Creates a instance. + /// + public GrainInterfaceType(string value) => _value = IdSpan.Create(value); + + /// + /// Creates a instance. + /// + public GrainInterfaceType(IdSpan value) => _value = value; + + /// + /// Returns the value underlying this instance. + /// + public IdSpan Value => _value; + + /// + /// Returns true if this value is equal to the instance. + /// + public bool IsDefault => _value.IsDefault; + + /// + /// Creates a instance. + /// + public static GrainInterfaceType Create(string value) => new GrainInterfaceType(value); + + /// + public override bool Equals(object? obj) => obj is GrainInterfaceType id && Equals(id); + + /// + public bool Equals(GrainInterfaceType other) => _value.Equals(other._value); + + /// + public override int GetHashCode() => _value.GetHashCode(); + + /// + /// Returns a UTF8 interpretation of the current instance. + /// + /// + public override string? ToString() => _value.ToString(); + + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString() ?? ""; + + bool ISpanFormattable.TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) + => _value.TryFormat(destination, out charsWritten); + + /// + /// Compares the provided operands for equality. + /// + /// The left operand. + /// The right operand. + /// if the provided values are equal, otherwise . + public static bool operator ==(GrainInterfaceType left, GrainInterfaceType right) => left.Equals(right); + + /// + /// Compares the provided operands for inequality. + /// + /// The left operand. + /// The right operand. + /// if the provided values are not equal, otherwise . + public static bool operator !=(GrainInterfaceType left, GrainInterfaceType right) => !left.Equals(right); + } + + /// + /// Gets a for an interface. + /// + public interface IGrainInterfaceTypeProvider + { + /// + /// Gets the corresponding to the specified . + /// + /// The grain interface type instance. + /// The resulting grain interface type identifier. + /// + /// if a corresponding to the provided type was found, otherwise . + /// + bool TryGetGrainInterfaceType(Type type, out GrainInterfaceType grainInterfaceType); + } + + /// + /// Gets a from attributes implementing . + /// + public class AttributeGrainInterfaceTypeProvider : IGrainInterfaceTypeProvider + { + /// + /// The service provider. + /// + private readonly IServiceProvider _serviceProvider; + + /// + /// Creates a instance. + /// + public AttributeGrainInterfaceTypeProvider(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + /// + public bool TryGetGrainInterfaceType(Type type, out GrainInterfaceType grainInterfaceType) + { + foreach (var attr in type.GetCustomAttributes(inherit: false)) + { + if (attr is IGrainInterfaceTypeProviderAttribute provider) + { + grainInterfaceType = provider.GetGrainInterfaceType(this._serviceProvider, type); + return true; + } + } + + grainInterfaceType = default; + return false; + } + } + + /// + /// An which implements this specifies the of the + /// type which it is attached to. + /// + public interface IGrainInterfaceTypeProviderAttribute + { + /// + /// Gets the grain interface identifier. + /// + /// The service provider. + /// The grain interface type. + /// + /// The corresponding to the provided type. + /// + GrainInterfaceType GetGrainInterfaceType(IServiceProvider services, Type type); + } + + /// + /// When applied to a grain interface, specifies the . + /// + [AttributeUsage(AttributeTargets.Interface, AllowMultiple = false)] + public sealed class GrainInterfaceTypeAttribute : Attribute, IGrainInterfaceTypeProviderAttribute + { + /// + /// The grain interface type. + /// + private readonly GrainInterfaceType _value; + + /// + /// Creates a instance. + /// + public GrainInterfaceTypeAttribute(string value) + { + _value = GrainInterfaceType.Create(value); + } + + /// + public GrainInterfaceType GetGrainInterfaceType(IServiceProvider services, Type type) => _value; + } +} diff --git a/src/Orleans.Persistence.Migration/Import/GrainInterfaceTypeResolver.cs b/src/Orleans.Persistence.Migration/Import/GrainInterfaceTypeResolver.cs new file mode 100644 index 0000000000..123d65e8be --- /dev/null +++ b/src/Orleans.Persistence.Migration/Import/GrainInterfaceTypeResolver.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Orleans.Runtime; +using Orleans.Serialization.TypeSystem; + +namespace Orleans.Metadata +{ + /// + /// Associates a with a . + /// + public class GrainInterfaceTypeResolver + { + private readonly IGrainInterfaceTypeProvider[] _providers; + private readonly TypeConverter _typeConverter; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The collection of grain interface type providers. + /// + /// + /// The type converter, used for generic parameter names. + /// + public GrainInterfaceTypeResolver( + IEnumerable providers, + TypeConverter typeConverter) + { + _providers = providers.ToArray(); + _typeConverter = typeConverter; + } + + /// + /// Returns the for the provided interface. + /// + /// The grain interface. + /// The for the provided interface. + public GrainInterfaceType GetGrainInterfaceType(Type type) + { + if (!type.IsInterface) + { + throw new ArgumentException($"Argument {nameof(type)} must be an interface. Provided value, \"{type}\", is not an interface.", nameof(type)); + } + + // Configured providers take precedence + foreach (var provider in _providers) + { + if (provider.TryGetGrainInterfaceType(type, out var interfaceType)) + { + interfaceType = AddGenericParameters(interfaceType, type); + return interfaceType; + } + } + + // Conventions are used as a fallback. + return GetGrainInterfaceTypeByConvention(type); + } + + /// + /// Gets a grain interface type based upon the default conventions. + /// + /// The grain interface type. + /// The grain interface type name. + public GrainInterfaceType GetGrainInterfaceTypeByConvention(Type type) + { + var result = GrainInterfaceType.Create(_typeConverter.Format(type, input => input switch + { + AssemblyQualifiedTypeSpec asm => asm.Type, // drop outer assembly qualification + _ => input + })); + + result = AddGenericParameters(result, type); + return result; + } + + private GrainInterfaceType AddGenericParameters(GrainInterfaceType result, Type type) + { + if (GenericGrainInterfaceType.TryParse(result, out var genericGrainType) + && type.IsConstructedGenericType + && !type.ContainsGenericParameters + && !genericGrainType.IsConstructed) + { + result = genericGrainType.Construct(_typeConverter, type.GetGenericArguments()).Value; + } + + return result; + } + } +} diff --git a/src/Orleans.Persistence.Migration/Import/GrainType.cs b/src/Orleans.Persistence.Migration/Import/GrainType.cs new file mode 100644 index 0000000000..aa2c805c73 --- /dev/null +++ b/src/Orleans.Persistence.Migration/Import/GrainType.cs @@ -0,0 +1,132 @@ +using System; +using System.Runtime.Serialization; +using System.Text; + +#nullable enable +namespace Orleans.Runtime +{ + /// + /// Represents the type of a grain. + /// + public readonly struct GrainType : IEquatable, IComparable, ISerializable, ISpanFormattable + { + private readonly IdSpan _value; + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The id. + /// + public GrainType(IdSpan id) => _value = id; + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The raw id value. + /// + public GrainType(byte[] value) => _value = new IdSpan(value); + + /// + /// Gets the underlying value. + /// + public IdSpan Value => _value; + + /// + /// Returns a span representation of this instance. + /// + /// + /// A representation of the value. + /// + public ReadOnlySpan AsSpan() => _value.AsSpan(); + + /// + /// Creates a new instance. + /// + /// + /// The value. + /// + /// + /// The newly created instance. + /// + public static GrainType Create(string value) => new GrainType(Encoding.UTF8.GetBytes(value)); + + /// + /// Converts a to a . + /// + /// The grain type to convert. + /// The corresponding . + public static explicit operator IdSpan(GrainType kind) => kind._value; + + /// + /// Converts a to a . + /// + /// The id span to convert. + /// The corresponding . + public static explicit operator GrainType(IdSpan id) => new GrainType(id); + + /// + /// Gets a value indicating whether this instance is the default value. + /// + public bool IsDefault => _value.IsDefault; + + /// + public override bool Equals(object? obj) => obj is GrainType kind && Equals(kind); + + /// + public bool Equals(GrainType obj) => _value.Equals(obj._value); + + /// + public override int GetHashCode() => _value.GetHashCode(); + + /// + /// Returns the array underlying a grain type instance. + /// + /// The grain type. + /// The array underlying a grain type instance. + /// + /// The returned array must not be modified. + /// + public static byte[]? UnsafeGetArray(GrainType id) => IdSpan.UnsafeGetArray(id._value); + + /// + public int CompareTo(GrainType other) => _value.CompareTo(other._value); + + /// + public void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue("v", IdSpan.UnsafeGetArray(_value)); + info.AddValue("h", _value.GetHashCode()); + } + + /// + /// Returns a string representation of this instance, decoding the value as UTF8. + /// + /// + /// A representation of this instance. + /// + public override string? ToString() => _value.ToString(); + + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString() ?? ""; + + bool ISpanFormattable.TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) + => _value.TryFormat(destination, out charsWritten); + + /// + /// Compares the provided operands for equality. + /// + /// The left operand. + /// The right operand. + /// if the provided values are equal, otherwise . + public static bool operator ==(GrainType left, GrainType right) => left.Equals(right); + + /// + /// Compares the provided operands for inequality. + /// + /// The left operand. + /// The right operand. + /// if the provided values are not equal, otherwise . + public static bool operator !=(GrainType left, GrainType right) => !(left == right); + } +} diff --git a/src/Orleans.Persistence.Migration/Import/GrainTypeResolver.cs b/src/Orleans.Persistence.Migration/Import/GrainTypeResolver.cs new file mode 100644 index 0000000000..31891047ce --- /dev/null +++ b/src/Orleans.Persistence.Migration/Import/GrainTypeResolver.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Orleans.Runtime; +using Orleans.Serialization.TypeSystem; + +namespace Orleans.Metadata +{ + /// + /// Associates a with a grain class. + /// + public class GrainTypeResolver + { + private const string GrainSuffix = "grain"; + private readonly IGrainTypeProvider[] _providers; + private readonly TypeConverter _typeConverter; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The grain type name providers. + /// + /// + /// The type converter, used to format generic parameters. + /// + public GrainTypeResolver( + IEnumerable resolvers, + TypeConverter argumentFormatter) + { + _providers = resolvers.ToArray(); + _typeConverter = argumentFormatter; + } + + /// + /// Returns the grain type for the provided class. + /// + /// The grain class. + /// The grain type for the provided class. + public GrainType GetGrainType(Type type) + { + if (!type.IsClass || type.IsAbstract) + { + throw new ArgumentException($"Argument {nameof(type)} must be a non-abstract class. Provided value, \"{type}\", is not a class.", nameof(type)); + } + + // Configured providers take precedence + foreach (var provider in _providers) + { + if (provider.TryGetGrainType(type, out var grainType)) + { + grainType = AddGenericParameters(grainType, type); + + return grainType; + } + } + + // Conventions are used as a fallback + return GetGrainTypeByConvention(type); + } + + private GrainType GetGrainTypeByConvention(Type type) + { + var name = type.Name.ToLowerInvariant(); + + // Trim generic arity + var index = name.IndexOf('`'); + if (index > 0) + { + name = name.Substring(0, index); + } + + // Trim "Grain" suffix + index = name.LastIndexOf(GrainSuffix); + if (index > 0 && name.Length - index == GrainSuffix.Length) + { + name = name.Substring(0, index); + } + + // Append the generic arity, eg typeof(MyListGrain) would eventually become mylist`1 + if (type.IsGenericType) + { + name = name + '`' + type.GetGenericArguments().Length; + } + + var grainType = GrainType.Create(name); + grainType = AddGenericParameters(grainType, type); + return grainType; + } + + private GrainType AddGenericParameters(GrainType grainType, Type type) + { + if (GenericGrainType.TryParse(grainType, out var genericGrainType) + && type.IsConstructedGenericType + && !type.ContainsGenericParameters + && !genericGrainType.IsConstructed) + { + grainType = genericGrainType.Construct(_typeConverter, type.GetGenericArguments()).GrainType; + } + + return grainType; + } + } +} diff --git a/src/Orleans.Persistence.Migration/Import/IGrainTypeProvider.cs b/src/Orleans.Persistence.Migration/Import/IGrainTypeProvider.cs new file mode 100644 index 0000000000..86e4349a4e --- /dev/null +++ b/src/Orleans.Persistence.Migration/Import/IGrainTypeProvider.cs @@ -0,0 +1,100 @@ +using System; +using Orleans.Metadata; +using Orleans.Runtime; + +namespace Orleans.Metadata +{ + /// + /// Associates a with a grain class. + /// + public interface IGrainTypeProvider + { + /// + /// Returns the grain type corresponding to the class identified by . + /// + bool TryGetGrainType(Type type, out GrainType grainType); + } + + /// + /// Gets the corresponding for a grain class from an + /// implementing on that class. + /// + public class AttributeGrainTypeProvider : IGrainTypeProvider + { + private readonly IServiceProvider _serviceProvider; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The service provider. + /// + public AttributeGrainTypeProvider(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + /// + public bool TryGetGrainType(Type grainClass, out GrainType grainType) + { + foreach (var attr in grainClass.GetCustomAttributes(inherit: false)) + { + if (attr is IGrainTypeProviderAttribute typeProviderAttribute) + { + grainType = typeProviderAttribute.GetGrainType(this._serviceProvider, grainClass); + return true; + } + } + + grainType = default; + return false; + } + } + + /// + /// Functionality which can be implemented by a custom which implements this specifies the of the + /// type which it is attached to. + /// + public interface IGrainTypeProviderAttribute + { + /// + /// Gets the for the attached . + /// + /// + /// The service provider. + /// + /// + /// The grain class. + /// + GrainType GetGrainType(IServiceProvider services, Type type); + } +} + +namespace Orleans +{ + /// + /// Specifies the grain type of the grain class which it is attached to. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public sealed class GrainTypeAttribute : Attribute, IGrainTypeProviderAttribute + { + /// + /// The grain type name. + /// + private readonly GrainType _grainType; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The grain type name. + /// + public GrainTypeAttribute(string grainType) + { + this._grainType = GrainType.Create(grainType); + } + + /// + public GrainType GetGrainType(IServiceProvider services, Type type) => this._grainType; + } +} diff --git a/src/Orleans.Persistence.Migration/Import/IdSpan.cs b/src/Orleans.Persistence.Migration/Import/IdSpan.cs new file mode 100644 index 0000000000..7da73c1b61 --- /dev/null +++ b/src/Orleans.Persistence.Migration/Import/IdSpan.cs @@ -0,0 +1,98 @@ +using System; +using System.Runtime.Serialization; +using System.Text; +using global::Orleans.CodeGeneration; +using global::Orleans.Concurrency; + +#nullable enable +namespace Orleans.Runtime +{ + /// + /// Primitive type for identities, representing a sequence of bytes. + /// + public readonly struct IdSpan : ISpanFormattable + { + /// + /// The underlying value. + /// + private readonly byte[]? _value; + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The value. + /// + public IdSpan(byte[] value) + { + _value = value; + } + + /// + /// Gets the underlying value. + /// + public ReadOnlyMemory Value => _value; + + /// + /// Gets a value indicating whether this instance is the default value. + /// + public bool IsDefault => _value is null || _value.Length == 0; + + /// + /// Creates a new instance from the provided value. + /// + /// + /// A new corresponding to the provided id. + /// + public static IdSpan Create(string? id) => id is string idString ? new IdSpan(Encoding.UTF8.GetBytes(idString)) : default; + + /// + /// Returns a span representation of this instance. + /// + /// + /// A span representation fo this instance. + /// + public ReadOnlySpan AsSpan() => _value; + + /// + /// Gets the underlying array from this instance. + /// + /// The id span. + /// The underlying array from this instance. + public static byte[]? UnsafeGetArray(IdSpan id) => id._value; + + /// + public int CompareTo(IdSpan other) => _value.AsSpan().SequenceCompareTo(other._value.AsSpan()); + + /// + /// Returns a string representation of this instance, decoding the value as UTF8. + /// + /// + /// A string representation fo this instance. + /// + public override string? ToString() => _value is null ? null : Encoding.UTF8.GetString(_value); + + public bool TryFormat(Span destination, out int charsWritten) + { + if (_value is null) + { + charsWritten = 0; + return true; + } + + var len = Encoding.UTF8.GetCharCount(_value); + if (destination.Length < len) + { + charsWritten = 0; + return false; + } + + charsWritten = Encoding.UTF8.GetChars(_value, destination); + return true; + } + + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString() ?? ""; + + bool ISpanFormattable.TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) => TryFormat(destination, out charsWritten); + } +} diff --git a/src/Orleans.Persistence.Migration/Import/TypeConverterExtensions.cs b/src/Orleans.Persistence.Migration/Import/TypeConverterExtensions.cs new file mode 100644 index 0000000000..fb31d8dbc7 --- /dev/null +++ b/src/Orleans.Persistence.Migration/Import/TypeConverterExtensions.cs @@ -0,0 +1,116 @@ +using System; +using System.Buffers.Text; +using System.Text; +using Orleans.Runtime; +using Orleans.Serialization.TypeSystem; + +namespace Orleans.Utilities +{ + /// + /// Extensions for working with . + /// + internal static class TypeConverterExtensions + { + private const char GenericTypeIndicator = '`'; + private const char StartArgument = '['; + + /// + /// Returns true if the provided type string is a generic type. + /// + public static bool IsGenericType(IdSpan type) => type.AsSpan().IndexOf((byte)GenericTypeIndicator) >= 0; + + /// + /// Returns true if the provided type string is a constructed generic type. + /// + public static bool IsConstructed(IdSpan type) => type.AsSpan().IndexOf((byte)StartArgument) > 0; + + /// + /// Returns the deconstructed form of the provided generic type. + /// + public static IdSpan GetDeconstructed(IdSpan type) + { + var span = type.AsSpan(); + var index = span.IndexOf((byte)StartArgument); + return index <= 0 ? type : new IdSpan(span.Slice(0, index).ToArray()); + } + + /// + /// Returns the constructed form of the provided generic type. + /// + public static IdSpan GetConstructed(this TypeConverter formatter, IdSpan unconstructed, params Type[] typeArguments) + { + var typeString = unconstructed.AsSpan(); + var indicatorIndex = typeString.IndexOf((byte)GenericTypeIndicator); + var arityString = typeString.Slice(indicatorIndex + 1); + if (indicatorIndex < 0 || arityString.IndexOf((byte)StartArgument) >= 0) + { + throw new InvalidOperationException("Cannot construct an already-constructed type"); + } + + if (!Utf8Parser.TryParse(arityString, out int arity, out var len) || len < arityString.Length || typeArguments.Length != arity) + { + throw new InvalidOperationException($"Insufficient number of type arguments, {typeArguments.Length}, provided while constructing type \"{unconstructed}\""); + } + + var typeSpecs = new TypeSpec[typeArguments.Length]; + for (var i = 0; i < typeArguments.Length; i++) + { + typeSpecs[i] = RuntimeTypeNameParser.Parse(formatter.Format(typeArguments[i])); + } + + var constructed = new ConstructedGenericTypeSpec(new NamedTypeSpec(null, unconstructed.ToString(), typeArguments.Length), typeArguments.Length, typeSpecs).Format(); + return IdSpan.Create(constructed); + } + + /// + /// Returns the constructed form of the provided generic grain type using the type arguments from the provided constructed interface type. + /// + public static GrainType GetConstructed(this GrainType grainType, GrainInterfaceType typeArguments) + { + var args = typeArguments.Value.AsSpan(); + var index = args.IndexOf((byte)StartArgument); + if (index <= 0) return grainType; // if no type arguments are provided, then the current logic expects the unconstructed form (but the grain call is going to fail later anyway...) + args = args.Slice(index); + + var type = grainType.Value.AsSpan(); + var buf = new byte[type.Length + args.Length]; + type.CopyTo(buf); + args.CopyTo(buf.AsSpan(type.Length)); + return new GrainType(buf); + } + + /// + /// Returns the type arguments for the provided constructed generic type string. + /// + public static Type[] GetArguments(this TypeConverter formatter, IdSpan constructed) + { + var str = constructed.AsSpan(); + var index = str.IndexOf((byte)StartArgument); + if (index <= 0) + { + return Array.Empty(); + } + + var safeString = "safer" + Encoding.UTF8.GetString(str.Slice(str.IndexOf((byte)GenericTypeIndicator))); + var parsed = RuntimeTypeNameParser.Parse(safeString); + if (!(parsed is ConstructedGenericTypeSpec spec)) + { + throw new InvalidOperationException($"Unable to correctly parse grain type {constructed}"); + } + + var result = new Type[spec.Arguments.Length]; + for (var i = 0; i < result.Length; i++) + { + var arg = spec.Arguments[i]; + var formattedArg = arg.Format(); + result[i] = formatter.Parse(formattedArg); + if (result[i] is null) + { + throw new InvalidOperationException($"Unable to parse argument \"{formattedArg}\" as a type for grain type \"{constructed}\""); + } + } + + return result; + } + } +} diff --git a/src/Orleans.Persistence.Migration/Orleans.Persistence.Migration.csproj b/src/Orleans.Persistence.Migration/Orleans.Persistence.Migration.csproj new file mode 100644 index 0000000000..9930394fca --- /dev/null +++ b/src/Orleans.Persistence.Migration/Orleans.Persistence.Migration.csproj @@ -0,0 +1,20 @@ + + + + Library + net7.0 + enable + disable + + + + + + + + + + + + + diff --git a/src/Orleans.Persistence.Migration/Serialization/OrleansJsonSerializer.cs b/src/Orleans.Persistence.Migration/Serialization/OrleansJsonSerializer.cs new file mode 100644 index 0000000000..1ea9a50d6a --- /dev/null +++ b/src/Orleans.Persistence.Migration/Serialization/OrleansJsonSerializer.cs @@ -0,0 +1,291 @@ +using System; +using System.Net; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Orleans.Runtime; +using Orleans.Serialization.TypeSystem; +using Microsoft.Extensions.Options; +using System.Globalization; + +namespace Orleans.Persistence.Migration.Serialization +{ + + /// + /// Utility class for configuring to support Orleans types. + /// + public class OrleansMigrationJsonSerializer + { + public const string UseFullAssemblyNamesProperty = "UseFullAssemblyNames"; + public const string IndentJsonProperty = "IndentJSON"; + public const string TypeNameHandlingProperty = "TypeNameHandling"; + private readonly JsonSerializerSettings settings; + + /// + /// Initializes a new instance of the class. + /// + public OrleansMigrationJsonSerializer(IOptions options) + { + this.settings = options.Value.JsonSerializerSettings; + } + + /// + /// Deserializes an object of the specified expected type from the provided input. + /// + /// The expected type. + /// The input. + /// The deserialized object. + public object Deserialize(Type expectedType, string input) + { + if (string.IsNullOrWhiteSpace(input)) + { + return null; + } + + return JsonConvert.DeserializeObject(input, expectedType, this.settings); + } + + /// + /// Serializes an object to a JSON string. + /// + /// The object to serialize. + /// The type the deserializer should expect. + public string Serialize(object item, Type expectedType) => JsonConvert.SerializeObject(item, expectedType, this.settings); + } + + /// + /// implementation for . + /// + /// + public class IPAddressConverter : JsonConverter + { + /// + public override bool CanConvert(Type objectType) + { + return (objectType == typeof(IPAddress)); + } + + /// + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + IPAddress ip = (IPAddress)value; + writer.WriteValue(ip.ToString()); + } + + /// + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + JToken token = JToken.Load(reader); + return IPAddress.Parse(token.Value()); + } + } + + /// + /// implementation for . + /// + /// + public class ActivationIdConverter : JsonConverter + { + /// + public override bool CanConvert(Type objectType) => objectType == typeof(ActivationId); + + /// + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + ActivationId id = (ActivationId)value; + writer.WriteValue(id.Key.ToString()); + } + + /// + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException(); + //return reader.Value switch + //{ + // string { Length: > 0 } str => ActivationId.FromParsableString(str), + // _ => default + //}; + } + } + + /// + /// implementation for . + /// + /// + public class SiloAddressJsonConverter : JsonConverter + { + /// + public override bool CanConvert(Type objectType) + { + return (objectType == typeof(SiloAddress)); + } + + /// + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + SiloAddress addr = (SiloAddress)value; + writer.WriteValue(addr.ToParsableString()); + } + + /// + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException(); + //switch (reader.TokenType) + //{ + // case JsonToken.StartObject: + // var jo = JObject.Load(reader); + // return SiloAddress.FromParsableString(jo["SiloAddress"].ToObject()); + // case JsonToken.String: + // return SiloAddress.FromParsableString(reader.Value as string); + //} + + //return null; + } + } + + /// + /// implementation for . + /// + /// + public class MembershipVersionJsonConverter : JsonConverter + { + /// + public override bool CanConvert(Type objectType) => objectType == typeof(MembershipVersion); + + /// + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + MembershipVersion typedValue = (MembershipVersion)value; + writer.WriteValue(long.Parse(typedValue.ToString())); + } + + /// + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException(); + //return reader.Value switch + //{ + // long l => new MembershipVersion(l), + // _ => default(MembershipVersion) + //}; + } + } + + /// + /// implementation for . + /// + /// + public class UniqueKeyConverter : JsonConverter + { + /// + public override bool CanConvert(Type objectType) + { + return (objectType == typeof(UniqueKey)); + } + + /// + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + UniqueKey key = (UniqueKey)value; + writer.WriteStartObject(); + writer.WritePropertyName("UniqueKey"); + writer.WriteValue(key.ToHexString()); + writer.WriteEndObject(); + } + + /// + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException(); + //JObject jo = JObject.Load(reader); + //UniqueKey addr = UniqueKey.Parse(jo["UniqueKey"].ToObject().AsSpan()); + //return addr; + } + } + + /// + /// implementation for . + /// + /// + public class IPEndPointConverter : JsonConverter + { + /// + public override bool CanConvert(Type objectType) + { + return (objectType == typeof(IPEndPoint)); + } + + /// + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + IPEndPoint ep = (IPEndPoint)value; + writer.WriteStartObject(); + writer.WritePropertyName("Address"); + serializer.Serialize(writer, ep.Address); + writer.WritePropertyName("Port"); + writer.WriteValue(ep.Port); + writer.WriteEndObject(); + } + + /// + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException(); + //JObject jo = JObject.Load(reader); + //IPAddress address = jo["Address"].ToObject(serializer); + //int port = jo["Port"].Value(); + //return new IPEndPoint(address, port); + } + } + + /// + /// implementation for . + /// + /// + public class GrainReferenceJsonConverter : JsonConverter + { + private static readonly Type AddressableType = typeof(IAddressable); + private readonly IGrainReferenceExtractor _grainIdExtractor; + + public GrainReferenceJsonConverter(IGrainReferenceExtractor grainIdExtractor) + { + _grainIdExtractor = grainIdExtractor; + } + + /// + public override bool CanConvert(Type objectType) + { + return AddressableType.IsAssignableFrom(objectType); + } + + /// + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var val = (GrainReference)value; + var (type, iface, key) = _grainIdExtractor.Extract(val); + writer.WriteStartObject(); + writer.WritePropertyName("Id"); + writer.WriteStartObject(); + writer.WritePropertyName("Type"); + writer.WriteValue(type.ToString()); + writer.WritePropertyName("Key"); + writer.WriteValue(key.ToString()); + writer.WriteEndObject(); + writer.WritePropertyName("Interface"); + writer.WriteValue(iface.ToString()); + writer.WriteEndObject(); + } + + /// + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException(); + //JObject jo = JObject.Load(reader); + //var id = jo["Id"]; + //GrainId grainId = GrainId.Create(id["Type"].ToObject(), id["Key"].ToObject()); + //var iface = GrainInterfaceType.Create(jo["Interface"].ToString()); + //return this.referenceActivator.CreateReference(grainId, iface); + } + } +} diff --git a/src/Orleans.Persistence.Migration/Serialization/OrleansJsonSerializerOptions.cs b/src/Orleans.Persistence.Migration/Serialization/OrleansJsonSerializerOptions.cs new file mode 100644 index 0000000000..a6f42dfba9 --- /dev/null +++ b/src/Orleans.Persistence.Migration/Serialization/OrleansJsonSerializerOptions.cs @@ -0,0 +1,31 @@ +using System; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace Orleans.Persistence.Migration.Serialization +{ + public class OrleansJsonSerializerOptions + { + public JsonSerializerSettings JsonSerializerSettings { get; set; } + + public OrleansJsonSerializerOptions() + { + JsonSerializerSettings = OrleansJsonSerializerSettings.GetDefaultSerializerSettings(); + } + } + + public class ConfigureOrleansJsonSerializerOptions : IPostConfigureOptions + { + private readonly IServiceProvider _serviceProvider; + + public ConfigureOrleansJsonSerializerOptions(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public void PostConfigure(string name, OrleansJsonSerializerOptions options) + { + OrleansJsonSerializerSettings.Configure(_serviceProvider, options.JsonSerializerSettings); + } + } +} diff --git a/src/Orleans.Persistence.Migration/Serialization/OrleansJsonSerializerSettings.cs b/src/Orleans.Persistence.Migration/Serialization/OrleansJsonSerializerSettings.cs new file mode 100644 index 0000000000..8c557f48e1 --- /dev/null +++ b/src/Orleans.Persistence.Migration/Serialization/OrleansJsonSerializerSettings.cs @@ -0,0 +1,86 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using Orleans.Serialization.TypeSystem; + +namespace Orleans.Persistence.Migration.Serialization +{ + public static class OrleansJsonSerializerSettings + { + internal static JsonSerializerSettings GetDefaultSerializerSettings() + { + return new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.All, + PreserveReferencesHandling = PreserveReferencesHandling.Objects, + DateFormatHandling = DateFormatHandling.IsoDateFormat, + DefaultValueHandling = DefaultValueHandling.Ignore, + MissingMemberHandling = MissingMemberHandling.Ignore, + NullValueHandling = NullValueHandling.Ignore, + ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, + TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, + Formatting = Formatting.None, + SerializationBinder = null, + }; + } + + /// + /// Returns the default serializer settings. + /// + /// + /// The service provider. + /// + /// The default serializer settings. + public static JsonSerializerSettings GetDefaultSerializerSettings(IServiceProvider services) + { + var settings = GetDefaultSerializerSettings(); + Configure(services, settings); + return settings; + } + + internal static void Configure(IServiceProvider services, JsonSerializerSettings jsonSerializerSettings) + { + //if (jsonSerializerSettings.SerializationBinder == null) + //{ + // var typeResolver = services.GetRequiredService(); + // jsonSerializerSettings.SerializationBinder = new OrleansJsonSerializationBinder(typeResolver); + //} + + jsonSerializerSettings.Converters.Add(new IPAddressConverter()); + jsonSerializerSettings.Converters.Add(new IPEndPointConverter()); + jsonSerializerSettings.Converters.Add(new ActivationIdConverter()); + jsonSerializerSettings.Converters.Add(new SiloAddressJsonConverter()); + jsonSerializerSettings.Converters.Add(new MembershipVersionJsonConverter()); + jsonSerializerSettings.Converters.Add(new UniqueKeyConverter()); + jsonSerializerSettings.Converters.Add(ActivatorUtilities.CreateInstance(services)); + } + + /// + /// Updates the provided serializer settings with the specified options. + /// + /// The settings. + /// if set to true, use full assembly-qualified names when formatting type names. + /// if set to true, indent the formatted JSON. + /// The type name handling options. + /// The provided serializer settings. + public static JsonSerializerSettings UpdateSerializerSettings(JsonSerializerSettings settings, bool useFullAssemblyNames, bool indentJson, TypeNameHandling? typeNameHandling) + { + if (useFullAssemblyNames) + { + settings.TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Full; + } + + if (indentJson) + { + settings.Formatting = Formatting.Indented; + } + + if (typeNameHandling.HasValue) + { + settings.TypeNameHandling = typeNameHandling.Value; + } + + return settings; + } + } +} \ No newline at end of file diff --git a/src/Orleans.Runtime/Properties/AssemblyInfo.cs b/src/Orleans.Runtime/Properties/AssemblyInfo.cs index fbd55079d3..42bf9b8450 100644 --- a/src/Orleans.Runtime/Properties/AssemblyInfo.cs +++ b/src/Orleans.Runtime/Properties/AssemblyInfo.cs @@ -12,5 +12,7 @@ [assembly: InternalsVisibleTo("TesterInternal")] [assembly: InternalsVisibleTo("TestInternalGrains")] +[assembly: InternalsVisibleTo("Orleans.Persistence.Migration")] + // Mocking libraries [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file diff --git a/test/Extensions/Migration.Tests/CsvDataReader.cs b/test/Extensions/Migration.Tests/CsvDataReader.cs new file mode 100644 index 0000000000..d579f08887 --- /dev/null +++ b/test/Extensions/Migration.Tests/CsvDataReader.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Xunit.Sdk; + +namespace Migration.Tests +{ + public class CsvDataReader : DataAttribute + { + private readonly string _filename; + + public CsvDataReader(string filename) + { + this._filename = filename; + } + + public override IEnumerable GetData(MethodInfo testMethod) + { + var parameters = testMethod.GetParameters(); + using var reader = new StreamReader(_filename); + string? line = null; + var lineNumber = 1; + while ((line = reader.ReadLine()) != null) + { + if (line.StartsWith('#')) + { + continue; + } + var rows = line.Split(';'); + if (rows.Length != parameters.Length) + { + throw new ArgumentException($"Invalid rows at line {lineNumber}"); + } + var results = new List(parameters.Length); + var n = 0; + foreach (var p in parameters) + { + switch ((Type.GetTypeCode(p.ParameterType))) + { + case TypeCode.Boolean: + results.Add(bool.Parse(rows[n])); + break; + case TypeCode.Int32: + results.Add(int.Parse(rows[n])); + break; + case TypeCode.String: + results.Add(rows[n].Trim('\"')); + break; + case TypeCode.Object: + if (!TryHandleObject(lineNumber, p, rows[n], out var obj)) + { + ThrowArgumentException(lineNumber, p); + } + results.Add(obj); + break; + default: + ThrowArgumentException(lineNumber, p); + break; + } + n++; + } + lineNumber++; + yield return results.ToArray(); + } + } + + private static bool TryHandleObject(int lineNumber, ParameterInfo parameter, string value, out object result) + { + try + { + if (parameter.ParameterType == typeof(Type)) + { + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + var type = assembly.GetType(value); + if (type != null) + { + if (type.IsGenericType && !type.IsConstructedGenericType) + { + var typeArguments = new List(); + foreach (var typeArgument in type.GetGenericArguments()) + { + // TODO be more clever? + //typeArguments.Add(typeArgument.GetType()); + typeArguments.Add(typeof(object)); + } + result = type.MakeGenericType(typeArguments.ToArray()); + } + else + { + result = type; + } + return true; + } + } + } + throw new Exception($"Type {value} not found"); + } + catch (Exception ex) + { + ThrowArgumentException(lineNumber, parameter, ex); + } + result = new object(); + return false; + } + + private static void ThrowArgumentException(int lineNumber, ParameterInfo p, Exception? inner = null) => throw new ArgumentException($"Line {lineNumber}: Type {p.ParameterType} not supported for parameter \"{p.Name}\"", inner); + } +} diff --git a/test/Extensions/Migration.Tests/GrainReferenceExtractorTests.cs b/test/Extensions/Migration.Tests/GrainReferenceExtractorTests.cs new file mode 100644 index 0000000000..a5b06c7a8a --- /dev/null +++ b/test/Extensions/Migration.Tests/GrainReferenceExtractorTests.cs @@ -0,0 +1,114 @@ +using Microsoft.Extensions.DependencyInjection; +using Orleans; +using Orleans.Persistence.Migration; +using Orleans.Runtime; +using Orleans.TestingHost; +using TestExtensions; +using Xunit; + +namespace Migration.Tests +{ + [TestCategory("Functionals"), TestCategory("Migration")] + public class GrainReferenceExtractorTests : HostedTestClusterEnsureDefaultStarted + { + private IGrainReferenceExtractor? _target; + + public GrainReferenceExtractorTests(MigrationDefaultClusterFixture fixture) + : base(fixture) + { + } + + private IGrainReferenceExtractor Target + { + get + { + if (_target == null) + { + var primarySilo = ((InProcessSiloHandle)this.HostedCluster.Primary).SiloHost; + _target = primarySilo.Services.GetRequiredService(); + } + return _target; + } + } + + [Fact] + public void ConvertStringKey() + { + var id = "hello"; + var expected = "mystring/hello"; + + var grain = this.GrainFactory.GetGrain(id); + var actual = Target.GetGrainId((GrainReference)grain); + + Assert.Equal(expected, actual); + } + + [Fact] + public void ConvertGuidKey() + { + var id = Guid.Parse("c53032c1-17a4-429c-a964-e61d65358214"); + var expected = "myguid/c53032c117a4429ca964e61d65358214"; + + var grain = this.GrainFactory.GetGrain(id); + var actual = Target.GetGrainId((GrainReference)grain); + + Assert.Equal(expected, actual); + } + + [Fact] + public void ConvertGuidCompoundKey() + { + var id = Guid.Parse("18832031-14df-4186-b6b4-1cd487b1cf7e"); + var ext = "hello"; + var expected = "myguidcompound/1883203114df4186b6b41cd487b1cf7e+hello"; + + var grain = this.GrainFactory.GetGrain(id, ext); + var actual = Target.GetGrainId((GrainReference)grain); + + Assert.Equal(expected, actual); + } + + [Fact] + public void ConvertIntegerKey() + { + var id = 806; + var expected = "myinteger/326"; + + var grain = this.GrainFactory.GetGrain(id); + var actual = Target.GetGrainId((GrainReference)grain); + + Assert.Equal(expected, actual); + } + + [Fact] + public void ConvertIntegerCompoundKey() + { + var id = 806; + var ext = "hello"; + var expected = "myintegercompound/326+hello"; + + var grain = this.GrainFactory.GetGrain(id, ext); + var actual = Target.GetGrainId((GrainReference)grain); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(typeof(IMyStringGrain), "hello", null, "mystring`1[[string]]/hello")] + [InlineData(typeof(IMyStringGrain), "hello", null, "mystring`2[[string],[int]]/hello")] + [InlineData(typeof(IMyGuidGrain), "50ff6a20-caf5-4e43-b27b-aac1825417dd", null, "myguid`1[[string]]/50ff6a20caf54e43b27baac1825417dd")] + [InlineData(typeof(IMyGuidGrain), "8b1a74a2-7cd5-4673-8df7-b69ecaf6eef3", null, "myguid`2[[string],[int]]/8b1a74a27cd546738df7b69ecaf6eef3")] + [InlineData(typeof(IMyGuidCompoundGrain), "42c9f497-0ae7-4ee6-8f07-447354ec4812", "hello", "myguidcompound`1[[string]]/42c9f4970ae74ee68f07447354ec4812+hello")] + [InlineData(typeof(IMyGuidCompoundGrain), "02fbd5e7-05c2-4e9b-b7de-807d1a2bbbad", "hello", "myguidcompound`2[[string],[int]]/02fbd5e705c24e9bb7de807d1a2bbbad+hello")] + [InlineData(typeof(IMyIntegerGrain), "806", null, "myinteger`1[[string]]/326")] + [InlineData(typeof(IMyIntegerGrain), "806", null, "myinteger`2[[string],[int]]/326")] + [InlineData(typeof(IMyIntegerCompoundGrain), "806", "hello", "myintegercompound`1[[string]]/326+hello")] + [InlineData(typeof(IMyIntegerCompoundGrain), "806", "hello", "myintegercompound`2[[string],[int]]/326+hello")] + public void ConvertGenericGrain(Type grainInterfaceType, string key, string keyExt, string expected) + { + var grain = this.GrainFactory.GetTestGrain(grainInterfaceType, key, keyExt); + var actual = Target.GetGrainId((GrainReference)grain); + Assert.Equal(expected, actual); + } + } +} \ No newline at end of file diff --git a/test/Extensions/Migration.Tests/Grains.cs b/test/Extensions/Migration.Tests/Grains.cs new file mode 100644 index 0000000000..ff2f025d73 --- /dev/null +++ b/test/Extensions/Migration.Tests/Grains.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Orleans; + +namespace Migration.Tests +{ + public interface IBase + { + Task Foo(); + } + + public interface IMyStringGrain : IGrainWithStringKey, IBase { } + + public interface IMyGuidGrain : IGrainWithGuidKey, IBase { } + + public interface IMyGuidCompoundGrain : IGrainWithGuidCompoundKey, IBase { } + + public interface IMyIntegerGrain : IGrainWithIntegerKey, IBase { } + + public interface IMyIntegerCompoundGrain : IGrainWithIntegerCompoundKey, IBase { } + + public interface IMyStringGrain : IGrainWithStringKey, IBase { } + + public interface IMyGuidGrain : IGrainWithGuidKey, IBase { } + + public interface IMyGuidCompoundGrain : IGrainWithGuidCompoundKey, IBase { } + + public interface IMyIntegerGrain : IGrainWithIntegerKey, IBase { } + + public interface IMyIntegerCompoundGrain : IGrainWithIntegerCompoundKey, IBase { } + + public interface IMyStringGrain : IGrainWithStringKey, IBase { } + + public interface IMyGuidGrain : IGrainWithGuidKey, IBase { } + + public interface IMyGuidCompoundGrain : IGrainWithGuidCompoundKey, IBase { } + + public interface IMyIntegerGrain : IGrainWithIntegerKey, IBase { } + + public interface IMyIntegerCompoundGrain : IGrainWithIntegerCompoundKey, IBase { } + + public abstract class Base : Grain, IBase + { + public Task Foo() => Task.FromResult(true); + } + public class MyStringGrain : Base, IMyStringGrain { } + + public class MyGuidGrain : Base, IMyGuidGrain { } + + public class MyGuidCompoundGrain : Base, IMyGuidCompoundGrain { } + + public class MyIntegerGrain : Base, IMyIntegerGrain { } + + public class MyIntegerCompoundGrain : Base, IMyIntegerCompoundGrain { } + + public class MyStringGrain : Base, IMyStringGrain { } + + public class MyGuidGrain : Base, IMyGuidGrain { } + + public class MyGuidCompoundGrain : Base, IMyGuidCompoundGrain { } + + public class MyIntegerGrain : Base, IMyIntegerGrain { } + + public class MyIntegerCompoundGrain : Base, IMyIntegerCompoundGrain { } + + public class MyStringGrain : Base, IMyStringGrain { } + + public class MyGuidGrain : Base, IMyGuidGrain { } + + public class MyGuidCompoundGrain : Base, IMyGuidCompoundGrain { } + + public class MyIntegerGrain : Base, IMyIntegerGrain { } + + public class MyIntegerCompoundGrain : Base, IMyIntegerCompoundGrain { } +} diff --git a/test/Extensions/Migration.Tests/JsonMigrationTests.cs b/test/Extensions/Migration.Tests/JsonMigrationTests.cs new file mode 100644 index 0000000000..4dd244217f --- /dev/null +++ b/test/Extensions/Migration.Tests/JsonMigrationTests.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.DependencyInjection; +using Orleans; +using Orleans.Persistence.Migration; +using Orleans.Persistence.Migration.Serialization; +using Orleans.Runtime; +using Orleans.TestingHost; +using TestExtensions; +using UnitTests.GrainInterfaces; +using Xunit; + +namespace Migration.Tests +{ + [TestCategory("Functionals"), TestCategory("Migration")] + public class JsonMigrationTests : HostedTestClusterEnsureDefaultStarted + { + private OrleansMigrationJsonSerializer? _target; + + public JsonMigrationTests(MigrationDefaultClusterFixture fixture) : base(fixture) + { + } + + private OrleansMigrationJsonSerializer Target + { + get + { + if (_target == null) + { + var primarySilo = ((InProcessSiloHandle)this.HostedCluster.Primary).SiloHost; + _target = primarySilo.Services.GetRequiredService(); + } + return _target; + } + } + + [SkippableTheory] + [CsvDataReader("grain-references.csv")] + public void GrainRefenceSerializerTest(Type grainInterfaceType, string key, string keyExt, string expected) + { + var grain = this.GrainFactory.GetTestGrain(grainInterfaceType, key, keyExt); + var actual = Target.Serialize(grain, grainInterfaceType); + Assert.Equal(expected, actual); + } + } +} \ No newline at end of file diff --git a/test/Extensions/Migration.Tests/Migration.Tests.csproj b/test/Extensions/Migration.Tests/Migration.Tests.csproj new file mode 100644 index 0000000000..daf26726a0 --- /dev/null +++ b/test/Extensions/Migration.Tests/Migration.Tests.csproj @@ -0,0 +1,35 @@ + + + + net7.0 + enable + enable + true + + + + + + + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/test/Extensions/Migration.Tests/MigrationDefaultClusterFixture.cs b/test/Extensions/Migration.Tests/MigrationDefaultClusterFixture.cs new file mode 100644 index 0000000000..72c186a479 --- /dev/null +++ b/test/Extensions/Migration.Tests/MigrationDefaultClusterFixture.cs @@ -0,0 +1,48 @@ +using Microsoft.Extensions.Logging; +using Orleans.Hosting; +using Orleans.TestingHost; +using Orleans; +using TestExtensions; +using Xunit; +using Orleans.Persistence.Migration; +using Microsoft.Extensions.DependencyInjection; + +namespace Migration.Tests +{ + // Assembly collections must be defined once in each assembly + [CollectionDefinition("DefaultCluster")] + public class DefaultClusterTestCollection : ICollectionFixture { } + + public class MigrationDefaultClusterFixture : DefaultClusterFixture, Xunit.IAsyncLifetime + { + static MigrationDefaultClusterFixture() + { + TestDefaultConfiguration.InitializeDefaults(); + } + + public override async Task InitializeAsync() + { + var builder = new TestClusterBuilder(1); + TestDefaultConfiguration.ConfigureTestCluster(builder); + builder.AddSiloBuilderConfigurator(); + + var testCluster = builder.Build(); + if (testCluster.Primary == null) + { + await testCluster.DeployAsync().ConfigureAwait(false); + } + + this.HostedCluster = testCluster; + this.Logger = this.Client?.ServiceProvider.GetRequiredService().CreateLogger("Application"); + } + + public class SiloConfigurator : ISiloConfigurator + { + public void Configure(ISiloBuilder hostBuilder) + { + hostBuilder + .AddMigrationTools(); + } + } + } +} diff --git a/test/Extensions/Migration.Tests/TestExtensions.cs b/test/Extensions/Migration.Tests/TestExtensions.cs new file mode 100644 index 0000000000..953fd83b46 --- /dev/null +++ b/test/Extensions/Migration.Tests/TestExtensions.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Orleans; + +namespace Migration.Tests +{ + internal static class TestExtensions + { + public static IGrain GetTestGrain(this IGrainFactory grainFactory, Type grainInterfaceType, string key, string keyExt) + { + IGrain grain; + if (grainInterfaceType.IsAssignableTo(typeof(IGrainWithStringKey))) + { + grain = grainFactory.GetGrain(grainInterfaceType, key); + } + else if (grainInterfaceType.IsAssignableTo(typeof(IGrainWithGuidKey))) + { + var keyGuid = Guid.Parse(key); + grain = grainFactory.GetGrain(grainInterfaceType, keyGuid); + } + else if (grainInterfaceType.IsAssignableTo(typeof(IGrainWithGuidCompoundKey))) + { + var keyGuid = Guid.Parse(key); + grain = grainFactory.GetGrain(grainInterfaceType, keyGuid, keyExt); + } + else if (grainInterfaceType.IsAssignableTo(typeof(IGrainWithIntegerKey))) + { + var keyInt = long.Parse(key); + grain = grainFactory.GetGrain(grainInterfaceType, keyInt); + } + else if (grainInterfaceType.IsAssignableTo(typeof(IGrainWithIntegerCompoundKey))) + { + var keyInt = long.Parse(key); + grain = grainFactory.GetGrain(grainInterfaceType, keyInt, keyExt); + } + else + { + throw new ArgumentException("Unknown key type"); + } + return grain; + } + } +} diff --git a/test/Extensions/Migration.Tests/grain-references.csv b/test/Extensions/Migration.Tests/grain-references.csv new file mode 100644 index 0000000000..2144c996bd --- /dev/null +++ b/test/Extensions/Migration.Tests/grain-references.csv @@ -0,0 +1,223 @@ +UnitTests.GrainInterfaces.IStateInheritanceTestGrain;24fe83a3-70fa-46db-bd12-ec5a75abd0cb;null;{"Id":{"Type":"stateinheritancetest","Key":"24fe83a370fa46dbbd12ec5a75abd0cb"},"Interface":"UnitTests.GrainInterfaces.IStateInheritanceTestGrain"} +UnitTests.GrainInterfaces.IReentrantStressTestGrain;755657987;null;{"Id":{"Type":"reentrantstresstest","Key":"2D0A6D03"},"Interface":"UnitTests.GrainInterfaces.IReentrantStressTestGrain"} +UnitTests.GrainInterfaces.IReminderTestGrain2;a2b9706c-3d82-46b1-9899-1bf83d6979bf;null;{"Id":{"Type":"remindertestgrain2","Key":"a2b9706c3d8246b198991bf83d6979bf"},"Interface":"UnitTests.GrainInterfaces.IReminderTestGrain2"} +UnitTests.GrainInterfaces.ICaterpillarGrain;1995669486;null;{"Id":{"Type":"caterpillar","Key":"76F37FEE"},"Interface":"UnitTests.GrainInterfaces.ICaterpillarGrain"} +UnitTests.GrainInterfaces.IGenericCastableGrain`1;390b977e-a8dd-4ac8-88c2-bbd0b78924a4;null;{"Id":{"Type":"genericcastable`1[[object]]","Key":"390b977ea8dd4ac888c2bbd0b78924a4"},"Interface":"UnitTests.GrainInterfaces.IGenericCastableGrain`1[[object]]"} +UnitTests.GrainInterfaces.IExtensionTestGrain;1937921045;null;{"Id":{"Type":"extensiontest","Key":"73825415"},"Interface":"UnitTests.GrainInterfaces.IExtensionTestGrain"} +TestGrainInterfaces.IGeneratedEventReporterGrain;a3848060-0813-4bb7-bd58-a253e5826792;null;{"Id":{"Type":"generatedeventreporter","Key":"a384806008134bb7bd58a253e5826792"},"Interface":"TestGrainInterfaces.IGeneratedEventReporterGrain"} +UnitTests.GrainInterfaces.IMessageSerializationGrain;1205989704;null;{"Id":{"Type":"messageserialization","Key":"47E1F148"},"Interface":"UnitTests.GrainInterfaces.IMessageSerializationGrain"} +Tester.IStorageDefaultFacetGrain;1634527589;null;{"Id":{"Type":"storagedefaultfacet","Key":"616CE965"},"Interface":"Tester.IStorageDefaultFacetGrain"} +UnitTests.GrainInterfaces.IIdleActivationGcTestGrain2;4a10a3b9-9298-42e9-9b09-8ae4a502f14f;null;{"Id":{"Type":"idleactivationgctestgrain2","Key":"4a10a3b9929842e99b098ae4a502f14f"},"Interface":"UnitTests.GrainInterfaces.IIdleActivationGcTestGrain2"} +UnitTests.GrainInterfaces.IGenericPingSelf`1;ebfd316d-f653-4ba0-bcd9-454f756f9a9c;null;{"Id":{"Type":"pingself`1[[object]]","Key":"ebfd316df6534ba0bcd9454f756f9a9c"},"Interface":"UnitTests.GrainInterfaces.IGenericPingSelf`1[[object]]"} +UnitTests.GrainInterfaces.IGenericReaderWriterGrain1`1;46192252;null;{"Id":{"Type":"genericreaderwritergrain1`1[[object]]","Key":"2C0D67C"},"Interface":"UnitTests.GrainInterfaces.IGenericReaderWriterGrain1`1[[object]]"} +UnitTests.GrainInterfaces.IGenericGrainWithGenericState`3;a004c722-022a-4da1-8f87-3dccd63b28b9;null;{"Id":{"Type":"genericgrainwithgenericstate`3[[object],[object],[object]]","Key":"a004c722022a4da18f873dccd63b28b9"},"Interface":"UnitTests.GrainInterfaces.IGenericGrainWithGenericState`3[[object],[object],[object]]"} +#UnitTests.GrainInterfaces.Generic.EdgeCases.IAnotherReceivingFurtherSpecializedGenArg`1;80bf7315-681e-4ead-8a9f-0db574778ae6;null;{"Id":{"Type":"grainforcastingbetweeninterfacesoffurtherspecializedgenargs`1[[object]]","Key":"80bf7315681e4ead8a9f0db574778ae6"},"Interface":"UnitTests.GrainInterfaces.Generic.EdgeCases.IAnotherReceivingFurtherSpecializedGenArg`1[[object]]"} +TestGrainInterfaces.IDoSomethingWithMoreEmptyGrain;1827812147;null;{"Id":{"Type":"dosomethingwithmoreempty","Key":"6CF23333"},"Interface":"TestGrainInterfaces.IDoSomethingWithMoreEmptyGrain"} +UnitTests.GrainInterfaces.IGrainStorageGenericGrain`1;1738239771;null;{"Id":{"Type":"grainstoragegeneric`1[[object]]","Key":"679B6F1B"},"Interface":"UnitTests.GrainInterfaces.IGrainStorageGenericGrain`1[[object]]"} +UnitTests.GrainInterfaces.ITailCallActivateDeactivateTestGrain;1021890673;null;{"Id":{"Type":"tailcallactivatedeactivatetest","Key":"3CE8D071"},"Interface":"UnitTests.GrainInterfaces.ITailCallActivateDeactivateTestGrain"} +UnitTests.GrainInterfaces.IFirstGrain;69aba5fd-956f-47e6-8443-20b73afba794;null;{"Id":{"Type":"first","Key":"69aba5fd956f47e6844320b73afba794"},"Interface":"UnitTests.GrainInterfaces.IFirstGrain"} +Tester.CodeGenTests.IGenericGrainWithGenericMethods`1;8fb373b6-af52-47d0-960f-73eb9d8ea872;null;{"Id":{"Type":"grainwithgenericmethods`1[[object]]","Key":"8fb373b6af5247d0960f73eb9d8ea872"},"Interface":"Tester.CodeGenTests.IGenericGrainWithGenericMethods`1[[object]]"} +UnitTests.GrainInterfaces.IDeadlockReentrantGrain;1587539937;null;{"Id":{"Type":"deadlockreentrant","Key":"5E9FEFE1"},"Interface":"UnitTests.GrainInterfaces.IDeadlockReentrantGrain"} +UnitTests.GrainInterfaces.IStreamInterceptionGrain;c37cf094-6548-4e49-bc24-6b87bb93aea2;null;{"Id":{"Type":"streaminterception","Key":"c37cf09465484e49bc246b87bb93aea2"},"Interface":"UnitTests.GrainInterfaces.IStreamInterceptionGrain"} +UnitTests.GrainInterfaces.IEchoTaskGrain;c4a32726-dbe2-4d2f-9ee1-79466f19918d;null;{"Id":{"Type":"echotask","Key":"c4a32726dbe24d2f9ee179466f19918d"},"Interface":"UnitTests.GrainInterfaces.IEchoTaskGrain"} +#UnitTests.GrainInterfaces.IGenericGrain`2;1160991143;null;{"Id":{"Type":"concretegrainwithgenericinterfaceoffloatstring","Key":"453351A7"},"Interface":"UnitTests.GrainInterfaces.IGenericGrain`2[[float],[string]]"} +UnitTests.GrainInterfaces.IFilteredImplicitSubscriptionGrain;5c13d6d7-625c-4b6e-98bd-79e568af420f;null;{"Id":{"Type":"filteredimplicitsubscription","Key":"5c13d6d7625c4b6e98bd79e568af420f"},"Interface":"UnitTests.GrainInterfaces.IFilteredImplicitSubscriptionGrain"} +UnitTests.GrainInterfaces.IUser;37e4f569-43ef-446e-8be5-c2911b41cf22;null;{"Id":{"Type":"user","Key":"37e4f56943ef446e8be5c2911b41cf22"},"Interface":"UnitTests.GrainInterfaces.IUser"} +UnitTests.GrainInterfaces.IStreamUnsubscribeTestGrain;395762917;null;{"Id":{"Type":"streamunsubscribetest","Key":"1796DCE5"},"Interface":"UnitTests.GrainInterfaces.IStreamUnsubscribeTestGrain"} +UnitTests.GrainInterfaces.IServiceType;356782155;null;{"Id":{"Type":"servicetype","Key":"1544104B"},"Interface":"UnitTests.GrainInterfaces.IServiceType"} +UnitTests.GrainInterfaces.IMayInterleavePredicateGrain;1675386945;null;{"Id":{"Type":"mayinterleavepredicate","Key":"63DC6041"},"Interface":"UnitTests.GrainInterfaces.IMayInterleavePredicateGrain"} +UnitTests.GrainInterfaces.ICatalogTestGrain;828571571;null;{"Id":{"Type":"catalogtest","Key":"3162FFB3"},"Interface":"UnitTests.GrainInterfaces.ICatalogTestGrain"} +UnitTests.GrainInterfaces.IGenericReaderWriterGrain3`3;570174651;null;{"Id":{"Type":"genericreaderwritergrain3`3[[object],[object],[object]]","Key":"21FC2CBB"},"Interface":"UnitTests.GrainInterfaces.IGenericReaderWriterGrain3`3[[object],[object],[object]]"} +UnitTests.GrainInterfaces.ITinyNameGrain`1;bab8b752-9f41-44af-81d1-ce1134146ffb;null;{"Id":{"Type":"tinyname`1[[object]]","Key":"bab8b7529f4144af81d1ce1134146ffb"},"Interface":"UnitTests.GrainInterfaces.ITinyNameGrain`1[[object]]"} +UnitTests.GrainInterfaces.IExternalTypeGrain;862292542;null;{"Id":{"Type":"externaltype","Key":"33658A3E"},"Interface":"UnitTests.GrainInterfaces.IExternalTypeGrain"} +UnitTests.GrainInterfaces.ITestGrainLongOnActivateAsync;483393641;null;{"Id":{"Type":"testgrainlongactivateasync","Key":"1CD00069"},"Interface":"UnitTests.GrainInterfaces.ITestGrainLongOnActivateAsync"} +UnitTests.GrainInterfaces.IDefaultPlacementGrain;6884294;null;{"Id":{"Type":"defaultplacement","Key":"690BC6"},"Interface":"UnitTests.GrainInterfaces.IDefaultPlacementGrain"} +UnitTests.GrainInterfaces.ICollectionSpecificAgeLimitForTenSecondsActivationGcTestGrain;5da9bf82-7a36-491a-94e2-b08d20d872c2;null;{"Id":{"Type":"collectionspecificagelimitfortensecondsactivationgctest","Key":"5da9bf827a36491a94e2b08d20d872c2"},"Interface":"UnitTests.GrainInterfaces.ICollectionSpecificAgeLimitForTenSecondsActivationGcTestGrain"} +UnitTests.GrainInterfaces.INoOpTestGrain;514265170;null;{"Id":{"Type":"nooptest","Key":"1EA71052"},"Interface":"UnitTests.GrainInterfaces.INoOpTestGrain"} +UnitTests.GrainInterfaces.IReentrantLocalStressTestGrain;342246466;null;{"Id":{"Type":"reentrantlocalstresstest","Key":"14664442"},"Interface":"UnitTests.GrainInterfaces.IReentrantLocalStressTestGrain"} +UnitTests.GrainInterfaces.IBasicGenericGrain`2;1699186584;null;{"Id":{"Type":"basicgeneric`2[[object],[object]]","Key":"65478798"},"Interface":"UnitTests.GrainInterfaces.IBasicGenericGrain`2[[object],[object]]"} +UnitTests.GrainInterfaces.IChainedGrain;690605625;null;{"Id":{"Type":"chained","Key":"2929CE39"},"Interface":"UnitTests.GrainInterfaces.IChainedGrain"} +UnitTests.GrainInterfaces.ISimpleGrain;2041792131;null;{"Id":{"Type":"simple","Key":"79B34683"},"Interface":"UnitTests.GrainInterfaces.ISimpleGrain"} +UnitTests.GrainInterfaces.IMultipleImplicitSubscriptionGrain;1e6df723-602d-416b-89fc-bbcabe7a595c;null;{"Id":{"Type":"multipleimplicitsubscription","Key":"1e6df723602d416b89fcbbcabe7a595c"},"Interface":"UnitTests.GrainInterfaces.IMultipleImplicitSubscriptionGrain"} +UnitTests.GrainInterfaces.ISimplePersistentGrain;23422281;null;{"Id":{"Type":"simplepersistent","Key":"1656549"},"Interface":"UnitTests.GrainInterfaces.ISimplePersistentGrain"} +UnitTests.GrainInterfaces.IStreamBatchingTestConsumerGrain;d7a520b5-512a-4ab7-9cd1-44d5cdefaff7;null;{"Id":{"Type":"batchingstreambatchingtestconsumer","Key":"d7a520b5512a4ab79cd144d5cdefaff7"},"Interface":"UnitTests.GrainInterfaces.IStreamBatchingTestConsumerGrain"} +#UnitTests.GrainInterfaces.Directories.IDefaultDirectoryGrain;65316db6-dc03-4f4c-9700-cdf416586dd5;null;{"Id":{"Type":"Default","Key":"65316db6dc034f4c9700cdf416586dd5"},"Interface":"UnitTests.GrainInterfaces.Directories.IDefaultDirectoryGrain"} +UnitTests.GrainInterfaces.IReentrantSelfManagedGrain;943281088;null;{"Id":{"Type":"reentrantselfmanagedgrain1","Key":"383953C0"},"Interface":"UnitTests.GrainInterfaces.IReentrantSelfManagedGrain"} +UnitTests.GrainInterfaces.ISiloRoleBasedPlacementGrain;DFpxK51laxdflE4;null;{"Id":{"Type":"silorolebasedplacement","Key":"DFpxK51laxdflE4"},"Interface":"UnitTests.GrainInterfaces.ISiloRoleBasedPlacementGrain"} +TestGrainInterfaces.IDoSomethingCombinedGrain;1768923680;null;{"Id":{"Type":"dosomethingcombined","Key":"696FA220"},"Interface":"TestGrainInterfaces.IDoSomethingCombinedGrain"} +#Orleans.IReminderTableGrain;1383025405;null;{"Id":{"Type":"remindertable","Key":"526F4AFD"},"Interface":"Orleans.IReminderTableGrain"} +UnitTests.GrainInterfaces.IBadProviderTestGrain;a8c5456a-176b-4c1c-9d06-7c4e3855e240;null;{"Id":{"Type":"badprovidertest","Key":"a8c5456a176b4c1c9d067c4e3855e240"},"Interface":"UnitTests.GrainInterfaces.IBadProviderTestGrain"} +UnitTests.GrainInterfaces.IJerk_ConsumerGrain;64e4de9f-61fe-485f-ac70-4eca3dc3a651;null;{"Id":{"Type":"jerk_consumer","Key":"64e4de9f61fe485fac704eca3dc3a651"},"Interface":"UnitTests.GrainInterfaces.IJerk_ConsumerGrain"} +#UnitTests.GrainInterfaces.ISimpleGenericGrain`1;1849756356;null;{"Id":{"Type":"specializedsimplegeneric","Key":"6E410AC4"},"Interface":"UnitTests.GrainInterfaces.ISimpleGenericGrain`1[[double]]"} +UnitTests.GrainInterfaces.IMultipleSubscriptionConsumerGrain;f2ec49ae-d3b6-48c7-bd2e-8eab9ef1ae3e;null;{"Id":{"Type":"multiplesubscriptionconsumer","Key":"f2ec49aed3b648c7bd2e8eab9ef1ae3e"},"Interface":"UnitTests.GrainInterfaces.IMultipleSubscriptionConsumerGrain"} +#UnitTests.GrainInterfaces.IHubGrain`3;944952858;null;{"Id":{"Type":"hub`3[[object],[object],[object]]","Key":"3852D61A"},"Interface":"UnitTests.GrainInterfaces.IHubGrain`3[[object],[object],[object]]"} +UnitTests.GrainInterfaces.ICollectionSpecificAgeLimitForZeroSecondsActivationGcTestGrain;b6716db3-d780-4732-95b9-13ec1aaea272;null;{"Id":{"Type":"collectionspecificagelimitforzerosecondsactivationgctest","Key":"b6716db3d780473295b913ec1aaea272"},"Interface":"UnitTests.GrainInterfaces.ICollectionSpecificAgeLimitForZeroSecondsActivationGcTestGrain"} +UnitTests.GrainInterfaces.IInternalGrainWithState;955224552;null;{"Id":{"Type":"internalgrainwithstate","Key":"38EF91E8"},"Interface":"UnitTests.GrainInterfaces.IInternalGrainWithState"} +UnitTests.GrainInterfaces.ISimpleGenericGrain1`1;733410875;null;{"Id":{"Type":"simplegenericgrain1`1[[object]]","Key":"2BB6F63B"},"Interface":"UnitTests.GrainInterfaces.ISimpleGenericGrain1`1[[object]]"} +UnitTests.GrainInterfaces.IGenericReaderWriterGrain2`2;2018078244;null;{"Id":{"Type":"genericreaderwritergrain2`2[[object],[object]]","Key":"78496E24"},"Interface":"UnitTests.GrainInterfaces.IGenericReaderWriterGrain2`2[[object],[object]]"} +UnitTests.GrainInterfaces.IFilteredImplicitSubscriptionWithExtensionGrain;8c798f10-37a3-4a1d-92cd-7145d1f1e478;lhmE0BpsVn;{"Id":{"Type":"filteredimplicitsubscriptionwithextension","Key":"8c798f1037a34a1d92cd7145d1f1e478+lhmE0BpsVn"},"Interface":"UnitTests.GrainInterfaces.IFilteredImplicitSubscriptionWithExtensionGrain"} +UnitTests.GrainInterfaces.IBusyActivationGcTestGrain2;7f6d37f9-83fc-41c9-a48d-838e54e422b0;null;{"Id":{"Type":"busyactivationgctestgrain2","Key":"7f6d37f983fc41c9a48d838e54e422b0"},"Interface":"UnitTests.GrainInterfaces.IBusyActivationGcTestGrain2"} +UnitTests.GrainInterfaces.IGeneratorTestDerivedFromCSharpInterfaceInExternalAssemblyGrain;1ea5f3ce-5171-41af-b37f-d0bd2c6a0f8f;null;{"Id":{"Type":"generatortestderivedfromcsharpinterfaceinexternalassembly","Key":"1ea5f3ce517141afb37fd0bd2c6a0f8f"},"Interface":"UnitTests.GrainInterfaces.IGeneratorTestDerivedFromCSharpInterfaceInExternalAssemblyGrain"} +UnitTests.GrainInterfaces.INonReentrantTaskGrain;448006073;null;{"Id":{"Type":"nonreentranttask","Key":"1AB407B9"},"Interface":"UnitTests.GrainInterfaces.INonReentrantTaskGrain"} +UnitTests.GrainInterfaces.IGeneratorTestGrain;258287485;null;{"Id":{"Type":"generatortest","Key":"F65277D"},"Interface":"UnitTests.GrainInterfaces.IGeneratorTestGrain"} +UnitTests.GrainInterfaces.IGrainStorageTestGrain;2c378eb4-bb75-4f93-9c7c-1ce0369a6506;null;{"Id":{"Type":"grainstoragetest","Key":"2c378eb4bb754f939c7c1ce0369a6506"},"Interface":"UnitTests.GrainInterfaces.IGrainStorageTestGrain"} +UnitTests.GrainInterfaces.ISimpleActivateDeactivateTestGrain;658690445;null;{"Id":{"Type":"simpleactivatedeactivatetest","Key":"2742D18D"},"Interface":"UnitTests.GrainInterfaces.ISimpleActivateDeactivateTestGrain"} +UnitTests.GrainInterfaces.IReminderTestCopyGrain;ea2789d7-44f3-4958-9dc2-0afe7dff40d8;null;{"Id":{"Type":"remindertestcopy","Key":"ea2789d744f349589dc20afe7dff40d8"},"Interface":"UnitTests.GrainInterfaces.IReminderTestCopyGrain"} +UnitTests.GrainInterfaces.IEchoGrain;76a6c14c-ffa1-490d-b91f-bd66a0aa0075;null;{"Id":{"Type":"echo","Key":"76a6c14cffa1490db91fbd66a0aa0075"},"Interface":"UnitTests.GrainInterfaces.IEchoGrain"} +UnitTests.GrainInterfaces.ITestGrain;88590172;null;{"Id":{"Type":"test","Key":"547C75C"},"Interface":"UnitTests.GrainInterfaces.ITestGrain"} +UnitTests.GrainInterfaces.IBlockingEchoTaskGrain;1159065763;null;{"Id":{"Type":"blockingechotask","Key":"4515F0A3"},"Interface":"UnitTests.GrainInterfaces.IBlockingEchoTaskGrain"} +UnitTests.GrainInterfaces.IDeadlockNonReentrantGrain;1875165821;null;{"Id":{"Type":"deadlocknonreentrant","Key":"6FC4C27D"},"Interface":"UnitTests.GrainInterfaces.IDeadlockNonReentrantGrain"} +UnitTests.GrainInterfaces.IFanOutACGrain;1897598064;null;{"Id":{"Type":"fanoutac","Key":"711B0C70"},"Interface":"UnitTests.GrainInterfaces.IFanOutACGrain"} +UnitTests.GrainInterfaces.ITimerCallGrain;2107110247;null;{"Id":{"Type":"timercall","Key":"7D97F367"},"Interface":"UnitTests.GrainInterfaces.ITimerCallGrain"} +UnitTests.Grains.IReducerGameGrain`2;JIqnEZeL2JLM0JM;null;{"Id":{"Type":"reducergame`2[[object],[object]]","Key":"JIqnEZeL2JLM0JM"},"Interface":"UnitTests.Grains.IReducerGameGrain`2[[object],[object]]"} +UnitTests.GrainInterfaces.ISimpleGenericGrain`1;723129487;null;{"Id":{"Type":"simplegeneric`1[[object]]","Key":"2B1A148F"},"Interface":"UnitTests.GrainInterfaces.ISimpleGenericGrain`1[[object]]"} +TestVersionGrainInterfaces.IVersionPlacementTestGrain;1128521097;null;{"Id":{"Type":"versionplacementtest","Key":"4343DD89"},"Interface":"TestVersionGrainInterfaces.IVersionPlacementTestGrain"} +UnitTests.GrainInterfaces.ICanBeOneWayGrain;293834f2-0d41-4fe1-881f-5e8d36bf44a2;null;{"Id":{"Type":"canbeoneway","Key":"293834f20d414fe1881f5e8d36bf44a2"},"Interface":"UnitTests.GrainInterfaces.ICanBeOneWayGrain"} +UnitTests.GrainInterfaces.ITaskActionActivateDeactivateTestGrain;1312023078;null;{"Id":{"Type":"taskactionactivatedeactivatetest","Key":"4E33E226"},"Interface":"UnitTests.GrainInterfaces.ITaskActionActivateDeactivateTestGrain"} +#UnitTests.GrainInterfaces.Generic.EdgeCases.IReceivingRearrangedGenArgs`2;bd005152-a3b4-4c4d-b30d-a9edc6d0f0c4;null;{"Id":{"Type":"specifyingsamegenargsbutrearranged`2[[object],[object]]","Key":"bd005152a3b44c4db30da9edc6d0f0c4"},"Interface":"UnitTests.GrainInterfaces.Generic.EdgeCases.IReceivingRearrangedGenArgs`2[[object],[object]]"} +UnitTests.GrainInterfaces.ISerializationGenerationGrain;1646290087;null;{"Id":{"Type":"serializationgeneration","Key":"622064A7"},"Interface":"UnitTests.GrainInterfaces.ISerializationGenerationGrain"} +UnitTests.GrainInterfaces.ISampleStreaming_ConsumerGrain;86f9135b-01c8-40a8-908d-fed05bd9146b;null;{"Id":{"Type":"samplestreaming_consumer","Key":"86f9135b01c840a8908dfed05bd9146b"},"Interface":"UnitTests.GrainInterfaces.ISampleStreaming_ConsumerGrain"} +UnitTests.GrainInterfaces.INonReentrantSelfManagedGrain;1637070772;null;{"Id":{"Type":"nonreentrantselfmanagedgrain1","Key":"6193B7B4"},"Interface":"UnitTests.GrainInterfaces.INonReentrantSelfManagedGrain"} +UnitTests.GrainInterfaces.IDeactivatingWhileActivatingTestGrain;1859580611;null;{"Id":{"Type":"deactivatingwhileactivatingtest","Key":"6ED6F2C3"},"Interface":"UnitTests.GrainInterfaces.IDeactivatingWhileActivatingTestGrain"} +UnitTests.GrainInterfaces.ISecondGrain;b12b7daa-8498-4018-8f3b-9d55f2f72091;null;{"Id":{"Type":"second","Key":"b12b7daa849840188f3b9d55f2f72091"},"Interface":"UnitTests.GrainInterfaces.ISecondGrain"} +UnitTests.GrainInterfaces.IBadConstructorTestGrain;1755131018;null;{"Id":{"Type":"badconstructortest","Key":"689D2C8A"},"Interface":"UnitTests.GrainInterfaces.IBadConstructorTestGrain"} +UnitTests.GrainInterfaces.IConcurrentGrain;2103906101;null;{"Id":{"Type":"concurrent","Key":"7D670F35"},"Interface":"UnitTests.GrainInterfaces.IConcurrentGrain"} +TestGrainInterfaces.IDoSomethingEmptyWithMoreGrain;1408560431;null;{"Id":{"Type":"dosomethingemptywithmore","Key":"53F4ED2F"},"Interface":"TestGrainInterfaces.IDoSomethingEmptyWithMoreGrain"} +UnitTests.GrainInterfaces.IExceptionGrain;1044172301;null;{"Id":{"Type":"exception","Key":"3E3CCE0D"},"Interface":"UnitTests.GrainInterfaces.IExceptionGrain"} +UnitTests.Stats.IStatsCollectorGrain;286970032;null;{"Id":{"Type":"statscollector","Key":"111AD0B0"},"Interface":"UnitTests.Stats.IStatsCollectorGrain"} +UnitTests.GrainInterfaces.IStringGrain;A5BE7bJI47ZpqU7;null;{"Id":{"Type":"string","Key":"A5BE7bJI47ZpqU7"},"Interface":"UnitTests.GrainInterfaces.IStringGrain"} +#Orleans.Streams.IPubSubRendezvousGrain;xjH6iYTUDIxunf0;null;{"Id":{"Type":"pubsubrendezvous","Key":"xjH6iYTUDIxunf0"},"Interface":"Orleans.Streams.IPubSubRendezvousGrain"} +UnitTests.GrainInterfaces.IEchoGenericChainGrain`1;250706793;null;{"Id":{"Type":"echogenericchain`1[[object]]","Key":"EF17B69"},"Interface":"UnitTests.GrainInterfaces.IEchoGenericChainGrain`1[[object]]"} +UnitTests.GrainInterfaces.IConcurrentReentrantGrain;1486414810;null;{"Id":{"Type":"concurrentreentrant","Key":"5898E3DA"},"Interface":"UnitTests.GrainInterfaces.IConcurrentReentrantGrain"} +UnitTests.GrainInterfaces.IBase4;32360848;null;{"Id":{"Type":"base4","Key":"1EDC990"},"Interface":"UnitTests.GrainInterfaces.IBase4"} +UnitTests.GrainInterfaces.IStreamLifecycleConsumerGrain;d6a083dd-868b-4901-88b0-226b2619b603;null;{"Id":{"Type":"streamlifecycleconsumer","Key":"d6a083dd868b490188b0226b2619b603"},"Interface":"UnitTests.GrainInterfaces.IStreamLifecycleConsumerGrain"} +UnitTests.GrainInterfaces.IAWSStorageTestGrain_GuidExtendedKey;f93fbd4e-1e10-4fff-b852-c65e3a760837;6tIiUwFEAF;{"Id":{"Type":"awsstoragetestgrainextendedkey","Key":"f93fbd4e1e104fffb852c65e3a760837+6tIiUwFEAF"},"Interface":"UnitTests.GrainInterfaces.IAWSStorageTestGrain_GuidExtendedKey"} +UnitTests.GrainInterfaces.IRequestContextProxyGrain;1185695571;null;{"Id":{"Type":"requestcontextproxy","Key":"46AC4753"},"Interface":"UnitTests.GrainInterfaces.IRequestContextProxyGrain"} +Orleans.Storage.IMemoryStorageGrain;915669526;null;{"Id":{"Type":"memorystorage","Key":"36940216"},"Interface":"Orleans.Storage.IMemoryStorageGrain"} +UnitTests.GrainInterfaces.IAWSStorageGenericGrain`1;1847177753;null;{"Id":{"Type":"awsstoragegeneric`1[[object]]","Key":"6E19B219"},"Interface":"UnitTests.GrainInterfaces.IAWSStorageGenericGrain`1[[object]]"} +UnitTests.GrainInterfaces.IClientAddressableTestGrain;965167004;null;{"Id":{"Type":"clientaddressabletest","Key":"3987479C"},"Interface":"UnitTests.GrainInterfaces.IClientAddressableTestGrain"} +UnitTests.GrainInterfaces.ILongRunningActivateDeactivateTestGrain;1354104491;null;{"Id":{"Type":"longrunningactivatedeactivatetest","Key":"50B5FEAB"},"Interface":"UnitTests.GrainInterfaces.ILongRunningActivateDeactivateTestGrain"} +UnitTests.GrainInterfaces.ILocalPlacementTestGrain;bb4b3d9b-936a-4276-979e-684241a1a7fc;null;{"Id":{"Type":"localplacementtest","Key":"bb4b3d9b936a4276979e684241a1a7fc"},"Interface":"UnitTests.GrainInterfaces.ILocalPlacementTestGrain"} +UnitTests.GrainInterfaces.IDIGrainWithInjectedServices;1822852202;null;{"Id":{"Type":"digrainwithinjectedservices","Key":"6CA6846A"},"Interface":"UnitTests.GrainInterfaces.IDIGrainWithInjectedServices"} +Tester.IStorageFacetGrain;199377300;null;{"Id":{"Type":"storagefacet","Key":"BE24194"},"Interface":"Tester.IStorageFacetGrain"} +Orleans.SqlUtils.StorageProvider.GrainInterfaces.IDeviceGrain;192d9915-b395-4574-bf36-eb234b85749b;null;{"Id":{"Type":"device","Key":"192d9915b3954574bf36eb234b85749b"},"Interface":"Orleans.SqlUtils.StorageProvider.GrainInterfaces.IDeviceGrain"} +UnitTests.GrainInterfaces.IPersistenceNoStateTestGrain;c8557410-38f7-4010-9aea-fb02d77ddec7;null;{"Id":{"Type":"persistencenostatetest","Key":"c855741038f740109aeafb02d77ddec7"},"Interface":"UnitTests.GrainInterfaces.IPersistenceNoStateTestGrain"} +UnitTests.GrainInterfaces.ILivenessTestGrain;377885765;null;{"Id":{"Type":"livenesstest","Key":"16861445"},"Interface":"UnitTests.GrainInterfaces.ILivenessTestGrain"} +UnitTests.GrainInterfaces.IGenericGrainWithNonGenericExtension`1;926477905;null;{"Id":{"Type":"genericgrainwithnongenericextension`1[[object]]","Key":"3738EE51"},"Interface":"UnitTests.GrainInterfaces.IGenericGrainWithNonGenericExtension`1[[object]]"} +UnitTests.GrainInterfaces.IReentrantTaskGrain;1592552292;null;{"Id":{"Type":"reentranttask","Key":"5EEC6B64"},"Interface":"UnitTests.GrainInterfaces.IReentrantTaskGrain"} +UnitTests.GrainInterfaces.IObserverGrain;1574307750;null;{"Id":{"Type":"observer","Key":"5DD607A6"},"Interface":"UnitTests.GrainInterfaces.IObserverGrain"} +UnitTests.GrainInterfaces.IHashBasedPlacementGrain;76ee920a-9f5f-4b3e-bbb4-26cd58adf8fe;null;{"Id":{"Type":"hashbasedbasedplacement","Key":"76ee920a9f5f4b3ebbb426cd58adf8fe"},"Interface":"UnitTests.GrainInterfaces.IHashBasedPlacementGrain"} +UnitTests.GrainInterfaces.IBase3;220333998;null;{"Id":{"Type":"basegrain1and2","Key":"D2207AE"},"Interface":"UnitTests.GrainInterfaces.IBase3"} +UnitTests.GrainInterfaces.INonReentrantGrain;2061829190;null;{"Id":{"Type":"nonrentrant","Key":"7AE50446"},"Interface":"UnitTests.GrainInterfaces.INonReentrantGrain"} +UnitTests.GrainInterfaces.ICollectionTestGrain;1465583634;null;{"Id":{"Type":"collectiontest","Key":"575B0812"},"Interface":"UnitTests.GrainInterfaces.ICollectionTestGrain"} +UnitTests.GrainInterfaces.IBadActivateDeactivateTestGrain;1425089949;null;{"Id":{"Type":"badactivatedeactivatetest","Key":"54F1259D"},"Interface":"UnitTests.GrainInterfaces.IBadActivateDeactivateTestGrain"} +UnitTests.GrainInterfaces.IRequestContextTaskGrain;482111775;null;{"Id":{"Type":"requestcontexttask","Key":"1CBC711F"},"Interface":"UnitTests.GrainInterfaces.IRequestContextTaskGrain"} +UnitTests.GrainInterfaces.IClientAddressableTestRendezvousGrain;723386915;null;{"Id":{"Type":"clientaddressabletestrendezvous","Key":"2B1E0223"},"Interface":"UnitTests.GrainInterfaces.IClientAddressableTestRendezvousGrain"} +UnitTests.GrainInterfaces.IConsumerEventCountingGrain;741c49d9-7c70-41a3-9729-a45af89565fa;null;{"Id":{"Type":"consumereventcounting","Key":"741c49d97c7041a39729a45af89565fa"},"Interface":"UnitTests.GrainInterfaces.IConsumerEventCountingGrain"} +Orleans.Runtime.Versions.IVersionStoreGrain;pXXfVhOgJUUoeKI;null;{"Id":{"Type":"versionstore","Key":"pXXfVhOgJUUoeKI"},"Interface":"Orleans.Runtime.Versions.IVersionStoreGrain"} +#UnitTests.GrainInterfaces.IGenericGrain`2;1817635725;null;{"Id":{"Type":"concretegrainwith2genericinterfaces","Key":"6C56EB8D"},"Interface":"UnitTests.GrainInterfaces.IGenericGrain`2[[int],[string]]"} +UnitTests.GrainInterfaces.ITimerRequestGrain;1213885935;null;{"Id":{"Type":"timerrequest","Key":"485A6DEF"},"Interface":"UnitTests.GrainInterfaces.ITimerRequestGrain"} +UnitTests.GrainInterfaces.IStreaming_ImplicitlySubscribedConsumerGrain;55a57a83-d210-476d-8308-cbaef151be50;null;{"Id":{"Type":"streaming_implicitlysubscribedconsumer","Key":"55a57a83d210476d8308cbaef151be50"},"Interface":"UnitTests.GrainInterfaces.IStreaming_ImplicitlySubscribedConsumerGrain"} +UnitTests.GrainInterfaces.IProxyGrain;286284312;null;{"Id":{"Type":"proxy","Key":"11105A18"},"Interface":"UnitTests.GrainInterfaces.IProxyGrain"} +UnitTests.GrainInterfaces.ISampleStreaming_InlineConsumerGrain;cfe49d85-b1f5-46af-b5a3-d38b1439ba7a;null;{"Id":{"Type":"samplestreaming_inlineconsumer","Key":"cfe49d85b1f546afb5a3d38b1439ba7a"},"Interface":"UnitTests.GrainInterfaces.ISampleStreaming_InlineConsumerGrain"} +TestGrainInterfaces.ICircularStateTestGrain;2eabbbe4-871d-4715-8a65-6db4448fa993;SLyqZ2l40l;{"Id":{"Type":"circularstatetest","Key":"2eabbbe4871d47158a656db4448fa993+SLyqZ2l40l"},"Interface":"TestGrainInterfaces.ICircularStateTestGrain"} +UnitTests.GrainInterfaces.IGrainWithNoProperties;140067825;null;{"Id":{"Type":"grainwithnoproperties","Key":"85943F1"},"Interface":"UnitTests.GrainInterfaces.IGrainWithNoProperties"} +UnitTests.GrainInterfaces.IStreamingImmutabilityTestGrain;9a693751-7aff-47e0-9a0e-7de03eff0bad;null;{"Id":{"Type":"streamingimmutabilitytest","Key":"9a6937517aff47e09a0e7de03eff0bad"},"Interface":"UnitTests.GrainInterfaces.IStreamingImmutabilityTestGrain"} +UnitTests.GrainInterfaces.IStatelessWorkerExceptionGrain;70456464;null;{"Id":{"Type":"statelessworkerexception","Key":"4331490"},"Interface":"UnitTests.GrainInterfaces.IStatelessWorkerExceptionGrain"} +UnitTests.GrainInterfaces.IDerivedFromBase;525799407;null;{"Id":{"Type":"derivedfrombase","Key":"1F570FEF"},"Interface":"UnitTests.GrainInterfaces.IDerivedFromBase"} +UnitTests.GrainInterfaces.IPromiseForwardGrain;1393761737;null;{"Id":{"Type":"promiseforward","Key":"53131DC9"},"Interface":"UnitTests.GrainInterfaces.IPromiseForwardGrain"} +UnitTests.GrainInterfaces.IMultifacetFactoryTestGrain;304666940;null;{"Id":{"Type":"multifacetfactorytest","Key":"1228D93C"},"Interface":"UnitTests.GrainInterfaces.IMultifacetFactoryTestGrain"} +UnitTests.GrainInterfaces.IIdleActivationGcTestGrain1;6d6931c2-3ec1-4549-8400-cf847b2c7b5f;null;{"Id":{"Type":"idleactivationgctestgrain1","Key":"6d6931c23ec145498400cf847b2c7b5f"},"Interface":"UnitTests.GrainInterfaces.IIdleActivationGcTestGrain1"} +UnitTests.GrainInterfaces.IRoundtripSerializationGrain;1891854233;null;{"Id":{"Type":"roundtripserialization","Key":"70C36799"},"Interface":"UnitTests.GrainInterfaces.IRoundtripSerializationGrain"} +#UnitTests.GrainInterfaces.Generic.EdgeCases.ISpecifyingGenArgsRepeatedlyToParentInterface`1;8783d760-71ac-467d-81ae-92ef65538fa5;null;{"Id":{"Type":"grainfortestingcastingbetweeninterfaceswithreusedgenargs","Key":"8783d76071ac467d81ae92ef65538fa5"},"Interface":"UnitTests.GrainInterfaces.Generic.EdgeCases.ISpecifyingGenArgsRepeatedlyToParentInterface`1[[bool]]"} +UnitTests.GrainInterfaces.IGrainWithListFields;78692298;null;{"Id":{"Type":"grainwithlistfields","Key":"4B0BFCA"},"Interface":"UnitTests.GrainInterfaces.IGrainWithListFields"} +Tester.CodeGenTests.IGrainWithGenericMethods;2d5c4e97-f034-456b-a0d9-d70c0d3b50d5;null;{"Id":{"Type":"grainwithgenericmethods","Key":"2d5c4e97f034456ba0d9d70c0d3b50d5"},"Interface":"Tester.CodeGenTests.IGrainWithGenericMethods"} +UnitTests.GrainInterfaces.INonReentrentStressGrainWithoutState;d4a4521b-e999-4a18-b24a-0c7ad8464408;null;{"Id":{"Type":"nonreentrentstressgrainwithoutstate","Key":"d4a4521be9994a18b24a0c7ad8464408"},"Interface":"UnitTests.GrainInterfaces.INonReentrentStressGrainWithoutState"} +UnitTests.GrainInterfaces.IInitialStateGrain;592689817;null;{"Id":{"Type":"initialstate","Key":"2353BA99"},"Interface":"UnitTests.GrainInterfaces.IInitialStateGrain"} +UnitTests.GrainInterfaces.ISampleStreaming_ProducerGrain;a6a8932b-e8f1-420e-bef2-562f354bdc56;null;{"Id":{"Type":"samplestreaming_producer","Key":"a6a8932be8f1420ebef2562f354bdc56"},"Interface":"UnitTests.GrainInterfaces.ISampleStreaming_ProducerGrain"} +UnitTests.GrainInterfaces.ICreateGrainReferenceTestGrain;166622289;null;{"Id":{"Type":"creategrainreferencetest","Key":"9EE7451"},"Interface":"UnitTests.GrainInterfaces.ICreateGrainReferenceTestGrain"} +UnitTests.GrainInterfaces.IServiceIdGrain;55fc723f-7d64-420a-9661-0a1c5c93d1a8;null;{"Id":{"Type":"serviceid","Key":"55fc723f7d64420a96610a1c5c93d1a8"},"Interface":"UnitTests.GrainInterfaces.IServiceIdGrain"} +UnitTests.GrainInterfaces.IPersistenceUserHandledErrorGrain;03a0c37b-fb76-4510-ae23-1b7ca9d72cbd;null;{"Id":{"Type":"persistenceuserhandlederror","Key":"03a0c37bfb764510ae231b7ca9d72cbd"},"Interface":"UnitTests.GrainInterfaces.IPersistenceUserHandledErrorGrain"} +UnitTests.GrainInterfaces.IClientAddressableTestConsumer;193080663;null;{"Id":{"Type":"clientaddressabletestconsumer","Key":"B822D57"},"Interface":"UnitTests.GrainIntefrfaces.IClientAddressableTestConsumer"} +UnitTests.GrainInterfaces.ITestContentGrain;609768672;null;{"Id":{"Type":"testcontent","Key":"245854E0"},"Interface":"UnitTests.GrainInterfaces.ITestContentGrain"} +UnitTests.GrainInterfaces.ISlowConsumingGrain;897d3182-7da0-49a3-9849-fa3d0bc84f45;null;{"Id":{"Type":"slowconsuming","Key":"897d31827da049a39849fa3d0bc84f45"},"Interface":"UnitTests.GrainInterfaces.ISlowConsumingGrain"} +Orleans.SqlUtils.StorageProvider.GrainInterfaces.ICustomerGrain;952648284;null;{"Id":{"Type":"customer","Key":"38C8425C"},"Interface":"Orleans.SqlUtils.StorageProvider.GrainInterfaces.ICustomerGrain"} +UnitTests.GrainInterfaces.INullStateGrain;277786123;null;{"Id":{"Type":"nullstate","Key":"108EAE0B"},"Interface":"UnitTests.GrainInterfaces.INullStateGrain"} +UnitTests.GrainInterfaces.IBusyActivationGcTestGrain1;afc4ee47-cdb7-4877-bb3b-993625104713;null;{"Id":{"Type":"busyactivationgctestgrain1","Key":"afc4ee47cdb74877bb3b993625104713"},"Interface":"UnitTests.GrainInterfaces.IBusyActivationGcTestGrain1"} +UnitTests.GrainInterfaces.IGeneratorTestDerivedFromFSharpInterfaceInExternalAssemblyGrain;78b24490-8df6-4b90-9634-b6e9d4c225a1;null;{"Id":{"Type":"generatortestderivedfromfsharpinterfaceinexternalassembly","Key":"78b244908df64b909634b6e9d4c225a1"},"Interface":"UnitTests.GrainInterfaces.IGeneratorTestDerivedFromFSharpInterfaceInExternalAssemblyGrain"} +UnitTests.GrainInterfaces.IStreamReliabilityTestGrain;1374906366;null;{"Id":{"Type":"streamreliabilitytest","Key":"51F367FE"},"Interface":"UnitTests.GrainInterfaces.IStreamReliabilityTestGrain"} +UnitTests.GrainInterfaces.ISimpleGenericGrain2`2;1440033047;null;{"Id":{"Type":"simplegenericgrain2`2[[object],[object]]","Key":"55D52917"},"Interface":"UnitTests.GrainInterfaces.ISimpleGenericGrain2`2[[object],[object]]"} +UnitTests.GrainInterfaces.Generic.EdgeCases.IPartiallySpecifyingInterface`1;15d006b9-837e-4fb3-b4d8-06aed0e2bb7b;null;{"Id":{"Type":"grainwithpartiallyspecifyinginterface`1[[object]]","Key":"15d006b9837e4fb3b4d806aed0e2bb7b"},"Interface":"UnitTests.GrainInterfaces.Generic.EdgeCases.IPartiallySpecifyingInterface`1[[object]]"} +#UnitTests.GrainInterfaces.Directories.ICustomDirectoryGrain;f55be117-f7cc-4578-b4be-eca58587b40b;null;{"Id":{"Type":"CustomGrainDirectory","Key":"f55be117f7cc4578b4beeca58587b40b"},"Interface":"UnitTests.GrainInterfaces.Directories.ICustomDirectoryGrain"} +UnitTests.GrainInterfaces.IGrainCallFilterTestGrain;1905260427;null;{"Id":{"Type":"graincallfiltertest","Key":"718FF78B"},"Interface":"UnitTests.GrainInterfaces.IGrainCallFilterTestGrain"} +UnitTests.GrainInterfaces.IRandomPlacementTestGrain;b059458d-2134-493b-9ab1-aa6c98fb7542;null;{"Id":{"Type":"randomplacementtest","Key":"b059458d2134493b9ab1aa6c98fb7542"},"Interface":"UnitTests.GrainInterfaces.IRandomPlacementTestGrain"} +Tester.CodeGenTests.INestedGenericGrain;0b3b039c-8073-45f7-8044-11aa2f53d35f;null;{"Id":{"Type":"nestedgeneric","Key":"0b3b039c807345f7804411aa2f53d35f"},"Interface":"Tester.CodeGenTests.INestedGenericGrain"} +UnitTests.GrainInterfaces.IStreaming_ProducerGrain;31351a5a-152d-4075-9e32-24c52b7b43c9;null;{"Id":{"Type":"streaming_producer","Key":"31351a5a152d40759e3224c52b7b43c9"},"Interface":"UnitTests.GrainInterfaces.IStreaming_ProducerGrain"} +UnitTests.GrainInterfaces.ISimpleGenericGrainUsingAzureStorageAndLongGrainName`1;c4f255ca-011c-4c08-be5c-002af31b2ccd;null;{"Id":{"Type":"simplegenericgrainusingazurestorageandlonggrainname`1[[object]]","Key":"c4f255ca011c4c08be5c002af31b2ccd"},"Interface":"UnitTests.GrainInterfaces.ISimpleGenericGrainUsingAzureStorageAndLongGrainName`1[[object]]"} +Tester.IStorageFactoryGrain;1357895488;null;{"Id":{"Type":"storagefactory","Key":"50EFD740"},"Interface":"Tester.IStorageFactoryGrain"} +UnitTests.GrainInterfaces.IPolymorphicTestGrain;740791152;null;{"Id":{"Type":"polymorphictest","Key":"2C279370"},"Interface":"UnitTests.GrainInterfaces.IPolymorphicTestGrain"} +#UnitTests.GrainInterfaces.IGenericGrain`2;1968490990;null;{"Id":{"Type":"concretegrainwithgenericinterfaceofintfloat","Key":"7554C9EE"},"Interface":"UnitTests.GrainInterfaces.IGenericGrain`2[[int],[float]]"} +TestVersionGrainInterfaces.IVersionUpgradeTestGrain;68627516;null;{"Id":{"Type":"versionupgradetest","Key":"4172C3C"},"Interface":"TestVersionGrainInterfaces.IVersionUpgradeTestGrain"} +UnitTests.GrainInterfaces.IActivateDeactivateWatcherGrain;1834318537;null;{"Id":{"Type":"activatedeactivatewatcher","Key":"6D557AC9"},"Interface":"UnitTests.GrainInterfaces.IActivateDeactivateWatcherGrain"} +UnitTests.GrainInterfaces.IStreamLifecycleProducerGrain;85072a9d-5fa8-4861-8968-eeee1556b85d;null;{"Id":{"Type":"streamlifecycleproducer","Key":"85072a9d5fa848618968eeee1556b85d"},"Interface":"UnitTests.GrainInterfaces.IStreamLifecycleProducerGrain"} +UnitTests.GrainInterfaces.IBase1;178772937;null;{"Id":{"Type":"basegrain1","Key":"AA7DBC9"},"Interface":"UnitTests.GrainInterfaces.IBase1"} +UnitTests.GrainInterfaces.IGuidGrain;ad2ed40d-bb6c-4138-b0f5-5ea926619866;null;{"Id":{"Type":"guid","Key":"ad2ed40dbb6c4138b0f55ea926619866"},"Interface":"UnitTests.GrainInterfaces.IGuidGrain"} +UnitTests.GrainInterfaces.IStreaming_ConsumerGrain;4ebc5892-900f-4770-a908-64f570dfb4b5;null;{"Id":{"Type":"streaming_consumer","Key":"4ebc5892900f4770a90864f570dfb4b5"},"Interface":"UnitTests.GrainInterfaces.IStreaming_ConsumerGrain"} +UnitTests.GrainInterfaces.IGenericGrainWithNoProperties`1;906681905;null;{"Id":{"Type":"genericgrainwithnoproperties`1[[object]]","Key":"360ADE31"},"Interface":"UnitTests.GrainInterfaces.IGenericGrainWithNoProperties`1[[object]]"} +UnitTests.GrainInterfaces.IStuckCleanGrain;6ea42d28-24a5-4e1e-8b56-d2271bfae74f;null;{"Id":{"Type":"stuckcleanup","Key":"6ea42d2824a54e1e8b56d2271bfae74f"},"Interface":"UnitTests.GrainInterfaces.IStuckCleanGrain"} +UnitTests.GrainInterfaces.IGrainServiceTestGrain;1727208869;null;{"Id":{"Type":"grainservicetest","Key":"66F31DA5"},"Interface":"UnitTests.GrainInterfaces.IGrainServiceTestGrain"} +UnitTests.Grains.ProgrammaticSubscribe.ISubscribeGrain;7991e49d-2fd9-48ef-bb16-24deee0b75d5;null;{"Id":{"Type":"subscribe","Key":"7991e49d2fd948efbb1624deee0b75d5"},"Interface":"UnitTests.Grains.ProgrammaticSubscribe.ISubscribeGrain"} +UnitTests.GrainInterfaces.IActivationCountBasedPlacementTestGrain;a9fa4cb3-ed96-47ed-83ad-c448742e3c6f;null;{"Id":{"Type":"activationcountbasedplacementtest","Key":"a9fa4cb3ed9647ed83adc448742e3c6f"},"Interface":"UnitTests.GrainInterfaces.IActivationCountBasedPlacementTestGrain"} +UnitTests.GrainInterfaces.IReentrantBlockingEchoTaskGrain;1978249119;null;{"Id":{"Type":"reentrantblockingechotask","Key":"75E9AF9F"},"Interface":"UnitTests.GrainInterfaces.IReentrantBlockingEchoTaskGrain"} +UnitTests.GrainInterfaces.ISerializerPresenceTest;85e647bc-d4be-43d8-9ee4-afe24410cc29;null;{"Id":{"Type":"serializerpresencetest","Key":"85e647bcd4be43d89ee4afe24410cc29"},"Interface":"UnitTests.GrainInterfaces.ISerializerPresenceTest"} +UnitTests.GrainInterfaces.IKeyExtensionTestGrain;ac26107f-057b-44bf-85b2-114fcfb924a7;SRKAs1lpVo;{"Id":{"Type":"keyextensiontest","Key":"ac26107f057b44bf85b2114fcfb924a7+SRKAs1lpVo"},"Interface":"UnitTests.GrainInterfaces.IKeyExtensionTestGrain"} +UnitTests.GrainInterfaces.IMemoryStorageTestGrain;a75123d6-7470-4be0-889c-19dd1ede2c85;null;{"Id":{"Type":"memorystoragetest","Key":"a75123d674704be0889c19dd1ede2c85"},"Interface":"UnitTests.GrainInterfaces.IMemoryStorageTestGrain"} +UnitTests.GrainInterfaces.IStatelessWorkerActivationCollectorTestGrain1;f4fd3a99-1f17-47cd-b79f-591e7f06510e;null;{"Id":{"Type":"statelessworkeractivationcollectortestgrain1","Key":"f4fd3a991f1747cdb79f591e7f06510e"},"Interface":"UnitTests.GrainInterfaces.IStatelessWorkerActivationCollectorTestGrain1"} +UnitTests.GrainInterfaces.ISimpleGenericGrainU`1;858065321;null;{"Id":{"Type":"simplegenericgrainu`1[[object]]","Key":"332509A9"},"Interface":"UnitTests.GrainInterfaces.ISimpleGenericGrainU`1[[object]]"} +UnitTests.GrainInterfaces.IGenericExtensionTestGrain`1;616469822;null;{"Id":{"Type":"genericextensiontest`1[[object]]","Key":"24BE953E"},"Interface":"UnitTests.GrainInterfaces.IGenericExtensionTestGrain`1[[object]]"} +#Tester.CodeGenTests.IRuntimeCodeGenGrain`1;320eb566-5a73-46eb-9535-8ec7a9d592b8;null;{"Id":{"Type":"runtimegeneric","Key":"320eb5665a7346eb95358ec7a9d592b8"},"Interface":"Tester.CodeGenTests.IRuntimeCodeGenGrain`1[[Tester.CodeGenTests.event,DefaultCluster.Tests]]"} +UnitTests.GrainInterfaces.IReentrentGrainWithState;8bbf124a-b224-43ab-ab36-58eb36b2a920;null;{"Id":{"Type":"reentrentgrainwithstate","Key":"8bbf124ab22443abab3658eb36b2a920"},"Interface":"UnitTests.GrainInterfaces.IReentrentGrainWithState"} +UnitTests.GrainInterfaces.IMultifacetTestGrain;739896949;null;{"Id":{"Type":"multifacettest","Key":"2C19EE75"},"Interface":"UnitTests.GrainInterfaces.IMultifacetTestGrain"} +UnitTests.GrainInterfaces.Generic.EdgeCases.ISpecifyingRearrangedGenArgsToParentInterface`2;597eff21-cc6c-4d1d-997d-87b9486e4b31;null;{"Id":{"Type":"grainfortestingcastingwithrearrangedgenargs`2[[object],[object]]","Key":"597eff21cc6c4d1d997d87b9486e4b31"},"Interface":"UnitTests.GrainInterfaces.Generic.EdgeCases.ISpecifyingRearrangedGenArgsToParentInterface`2[[object],[object]]"} +UnitTests.GrainInterfaces.IImplicitSubscriptionCounterGrain;34e1f483-823c-46d7-8519-5f68b61c8f13;null;{"Id":{"Type":"implicitsubscriptioncounter","Key":"34e1f483823c46d785195f68b61c8f13"},"Interface":"UnitTests.GrainInterfaces.IImplicitSubscriptionCounterGrain"} +UnitTests.GrainInterfaces.IStressTestGrain;1198893283;null;{"Id":{"Type":"stresstest","Key":"4775A8E3"},"Interface":"UnitTests.GrainInterfaces.IStressTestGrain"} +UnitTests.GrainInterfaces.IReentrantGrain;1359246283;null;{"Id":{"Type":"reentrant","Key":"510473CB"},"Interface":"UnitTests.GrainInterfaces.IReentrantGrain"} +UnitTests.GrainInterfaces.INonGenericCastableGrain;dd734300-4d47-4037-897a-3fbe15aa87f2;null;{"Id":{"Type":"nongenericcastable","Key":"dd7343004d474037897a3fbe15aa87f2"},"Interface":"UnitTests.GrainInterfaces.INonGenericCastableGrain"} +UnitTests.GrainInterfaces.IReminderTestGrain;856837668;null;{"Id":{"Type":"remindertest","Key":"33124E24"},"Interface":"UnitTests.GrainInterfaces.IReminderTestGrain"} +UnitTests.GrainInterfaces.IOneWayGrain;904cc550-f5d7-47e9-8571-a3e38d17800b;null;{"Id":{"Type":"oneway","Key":"904cc550f5d747e98571a3e38d17800b"},"Interface":"UnitTests.GrainInterfaces.IOneWayGrain"} +Tester.IStorageDefaultFactoryGrain;1507581294;null;{"Id":{"Type":"storagedefaultfactory","Key":"59DBDD6E"},"Interface":"Tester.IStorageDefaultFactoryGrain"} +UnitTests.GrainInterfaces.IUnorderedNonReentrantGrain;814782720;null;{"Id":{"Type":"unorderednonrentrant","Key":"30909900"},"Interface":"UnitTests.GrainInterfaces.IUnorderedNonReentrantGrain"} +UnitTests.GrainInterfaces.ITimerGrain;1843344799;null;{"Id":{"Type":"timer","Key":"6DDF359F"},"Interface":"UnitTests.GrainInterfaces.ITimerGrain"} +UnitTests.GrainInterfaces.IRequestContextTestGrain;479173056;null;{"Id":{"Type":"requestcontexttest","Key":"1C8F99C0"},"Interface":"UnitTests.GrainInterfaces.IRequestContextTestGrain"} +UnitTests.GrainInterfaces.IPassive_ConsumerGrain;1bc0fe28-f25f-4f7e-bd85-87be738c3c5b;null;{"Id":{"Type":"passive_consumer","Key":"1bc0fe28f25f4f7ebd8587be738c3c5b"},"Interface":"UnitTests.GrainInterfaces.IPassive_ConsumerGrain"} +UnitTests.GrainInterfaces.Generic.EdgeCases.IArbitraryInterface`2;0446193e-a3f5-40b3-a24a-c6c678893f08;null;{"Id":{"Type":"grainwithgenargsunrelatedtofullyspecifiedgenericinterface`2[[object],[object]]","Key":"0446193ea3f540b3a24ac6c678893f08"},"Interface":"UnitTests.GrainInterfaces.Generic.EdgeCases.IArbitraryInterface`2[[object],[object]]"} +UnitTests.GrainInterfaces.IGenericGrainWithListFields`1;100287514;null;{"Id":{"Type":"genericgrainwithlistfields`1[[object]]","Key":"5FA441A"},"Interface":"UnitTests.GrainInterfaces.IGenericGrainWithListFields`1[[object]]"} +Orleans.Runtime.IManagementGrain;985898170;null;{"Id":{"Type":"management","Key":"3AC39CBA"},"Interface":"Orleans.Runtime.IManagementGrain"} +UnitTests.GrainInterfaces.IPersistenceProviderErrorGrain;0db57901-653e-46cc-a1f0-05f34480e3c1;null;{"Id":{"Type":"persistenceprovidererror","Key":"0db57901653e46cca1f005f34480e3c1"},"Interface":"UnitTests.GrainInterfaces.IPersistenceProviderErrorGrain"} +UnitTests.GrainInterfaces.IFanOutGrain;1030223548;null;{"Id":{"Type":"fanout","Key":"3D67F6BC"},"Interface":"UnitTests.GrainInterfaces.IFanOutGrain"} +UnitTests.GrainInterfaces.IReminderGrainWrong;221120712;null;{"Id":{"Type":"wrongreminder","Key":"D2E08C8"},"Interface":"UnitTests.GrainInterfaces.IReminderGrainWrong"} +UnitTests.GrainInterfaces.IValueTypeTestGrain;e3b5a093-c5d6-4823-b26d-7cb6e60a4bbd;null;{"Id":{"Type":"valuetypetest","Key":"e3b5a093c5d64823b26d7cb6e60a4bbd"},"Interface":"UnitTests.GrainInterfaces.IValueTypeTestGrain"} +UnitTests.GrainInterfaces.ITrickyMethodInterceptionGrain;1884440856;null;{"Id":{"Type":"trickyinterception","Key":"70524918"},"Interface":"UnitTests.GrainInterfaces.ITrickyMethodInterceptionGrain"} +UnitTests.GrainInterfaces.IStreaming_ProducerConsumerGrain;158cac19-73d5-43fa-8c87-2e32289c942d;null;{"Id":{"Type":"streaming_producerconsumer","Key":"158cac1973d543fa8c872e32289c942d"},"Interface":"UnitTests.GrainInterfaces.IStreaming_ProducerConsumerGrain"} +Orleans.TestingHost.IStorageFaultGrain;ZJBNggZFRMHcTqg;null;{"Id":{"Type":"storagefault","Key":"ZJBNggZFRMHcTqg"},"Interface":"Orleans.TestingHost.IStorageFaultGrain"} +TestGrainInterfaces.IDoSomethingWithMoreGrain;2048998678;null;{"Id":{"Type":"dosomethingwithmore","Key":"7A213D16"},"Interface":"TestGrainInterfaces.IDoSomethingWithMoreGrain"} +TestGrainInterfaces.IGeneratedEventCollectorGrain;85a6be13-b09a-4d1b-9a4d-b5140f442c8c;null;{"Id":{"Type":"generatedeventcollector","Key":"85a6be13b09a4d1b9a4db5140f442c8c"},"Interface":"TestGrainInterfaces.IGeneratedEventCollectorGrain"} +UnitTests.GrainInterfaces.ISimpleObserverableGrain;193837295;null;{"Id":{"Type":"simpleobserverable","Key":"B8DB8EF"},"Interface":"UnitTests.GrainInterfaces.ISimpleObserverableGrain"} +UnitTests.GrainInterfaces.IGuidTestGrain;f4e872e5-f92c-4bd4-9021-82a62170f07c;null;{"Id":{"Type":"guidtest","Key":"f4e872e5f92c4bd4902182a62170f07c"},"Interface":"UnitTests.GrainInterfaces.IGuidTestGrain"} +UnitTests.GrainInterfaces.IStuckGrain;e618003d-d45e-40d9-8f2b-69227c6b5e73;null;{"Id":{"Type":"stuck","Key":"e618003dd45e40d98f2b69227c6b5e73"},"Interface":"UnitTests.GrainInterfaces.IStuckGrain"} +UnitTests.GrainInterfaces.IAWSStorageTestGrain;624ffd51-e87b-4213-9604-76eae4b9b8f0;null;{"Id":{"Type":"awsstoragetest","Key":"624ffd51e87b4213960476eae4b9b8f0"},"Interface":"UnitTests.GrainInterfaces.IAWSStorageTestGrain"} +UnitTests.GrainInterfaces.IStatelessWorkerGrain;1122849376;null;{"Id":{"Type":"statelessworker","Key":"42ED5260"},"Interface":"UnitTests.GrainInterfaces.IStatelessWorkerGrain"} +Orleans.Providers.IMemoryStreamQueueGrain;cccd036d-53b0-4e5b-8ea0-3a5934fec94e;null;{"Id":{"Type":"memorystreamqueue","Key":"cccd036d53b04e5b8ea03a5934fec94e"},"Interface":"Orleans.Providers.IMemoryStreamQueueGrain"} +UnitTests.GrainInterfaces.IPreferLocalPlacementTestGrain;ada7444b-3b5c-4bc2-935f-3dec29780836;null;{"Id":{"Type":"preferlocalplacementtest","Key":"ada7444b3b5c4bc2935f3dec29780836"},"Interface":"UnitTests.GrainInterfaces.IPreferLocalPlacementTestGrain"} +UnitTests.GrainInterfaces.IGenericMethodInterceptionGrain`1;104430768;null;{"Id":{"Type":"genericmethodinterception`1[[object]]","Key":"6397CB0"},"Interface":"UnitTests.GrainInterfaces.IGenericMethodInterceptionGrain`1[[object]]"} +UnitTests.GrainInterfaces.IPersistenceTestGrain;6a4bc760-8a4a-43bc-9183-0691065e6da7;null;{"Id":{"Type":"persistencetest","Key":"6a4bc7608a4a43bc91830691065e6da7"},"Interface":"UnitTests.GrainInterfaces.IPersistenceTestGrain"} +UnitTests.GrainInterfaces.ILongRunningTaskGrain`1;0cf15a94-e1e7-4f57-a45c-5f61cc4a4481;null;{"Id":{"Type":"longrunningtask`1[[object]]","Key":"0cf15a94e1e74f57a45c5f61cc4a4481"},"Interface":"UnitTests.GrainInterfaces.ILongRunningTaskGrain`1[[object]]"} +TestGrainInterfaces.IDoSomethingEmptyGrain;1532355164;null;{"Id":{"Type":"dosomethingempty","Key":"5B55E25C"},"Interface":"TestGrainInterfaces.IDoSomethingEmptyGrain"} +UnitTests.GrainInterfaces.IThirdGrain;d7ZzZFS7TPTOgu0;null;{"Id":{"Type":"third","Key":"d7ZzZFS7TPTOgu0"},"Interface":"UnitTests.GrainInterfaces.IThirdGrain"} +UnitTests.GrainInterfaces.IDbGrain`1;1001309640;null;{"Id":{"Type":"db`1[[object]]","Key":"3BAEC5C8"},"Interface":"UnitTests.GrainInterfaces.IDbGrain`1[[object]]"} +Tester.StreamingTests.ILeaseManagerGrain;CXfRe6lQn3Da2ET;null;{"Id":{"Type":"leasemanager","Key":"CXfRe6lQn3Da2ET"},"Interface":"Tester.StreamingTests.ILeaseManagerGrain"} +UnitTests.GrainInterfaces.ILocalContentGrain;484fe9b3-6b64-4718-a588-030be4750edf;null;{"Id":{"Type":"localcontent","Key":"484fe9b36b644718a588030be4750edf"},"Interface":"UnitTests.GrainInterfaces.ILocalContentGrain"} +UnitTests.GrainInterfaces.IFaultableConsumerGrain;4446fd3f-e1e9-4be3-a176-8866a004b42a;null;{"Id":{"Type":"faultableconsumer","Key":"4446fd3fe1e94be3a1768866a004b42a"},"Interface":"UnitTests.GrainInterfaces.IFaultableConsumerGrain"} +UnitTests.GrainInterfaces.IPersistenceErrorGrain;e932b7b6-9e5e-4a15-b0c4-3098475ac930;null;{"Id":{"Type":"persistenceerror","Key":"e932b7b69e5e4a15b0c43098475ac930"},"Interface":"UnitTests.GrainInterfaces.IPersistenceErrorGrain"} +Orleans.Runtime.Development.IDevelopmentLeaseProviderGrain;762937865;null;{"Id":{"Type":"developmentleaseprovider","Key":"2D798209"},"Interface":"Orleans.Runtime.Development.IDevelopmentLeaseProviderGrain"} +UnitTests.GrainInterfaces.IPersistenceProviderErrorProxyGrain;6dcafbb5-7c30-4d64-ba3a-2a2f63236d96;null;{"Id":{"Type":"persistenceprovidererrorproxy","Key":"6dcafbb57c304d64ba3a2a2f63236d96"},"Interface":"UnitTests.GrainInterfaces.IPersistenceProviderErrorProxyGrain"} diff --git a/test/TestInfrastructure/TestExtensions/DefaultClusterFixture.cs b/test/TestInfrastructure/TestExtensions/DefaultClusterFixture.cs index 408fd3d739..a1426c5426 100644 --- a/test/TestInfrastructure/TestExtensions/DefaultClusterFixture.cs +++ b/test/TestInfrastructure/TestExtensions/DefaultClusterFixture.cs @@ -16,13 +16,13 @@ static DefaultClusterFixture() TestDefaultConfiguration.InitializeDefaults(); } - public TestCluster HostedCluster { get; private set; } + public TestCluster HostedCluster { get; protected set; } public IGrainFactory GrainFactory => this.HostedCluster?.GrainFactory; public IClusterClient Client => this.HostedCluster?.Client; - public ILogger Logger { get; private set; } + public ILogger Logger { get; protected set; } public virtual async Task InitializeAsync() { From 78af76d3feb8a7a00d2fba6dc5930ca412a892e0 Mon Sep 17 00:00:00 2001 From: Benjamin Petit Date: Wed, 7 Dec 2022 13:35:45 +0100 Subject: [PATCH 2/5] Added base list operation on IGrainStorage --- Orleans.sln | 17 ++++++++++++++++- .../Provider/DynamoDBGrainStorage.cs | 2 ++ .../Storage/Provider/AdoNetGrainStorage.cs | 2 ++ .../Providers/Storage/AzureBlobStorage.cs | 4 ++++ .../Providers/Storage/AzureTableStorage.cs | 2 ++ src/Orleans.Core/Providers/IGrainStorage.cs | 17 ++++++++++++++++- .../FaultInjectionStorageProvider.cs | 3 +++ src/OrleansProviders/Storage/MemoryStorage.cs | 2 ++ .../Storage/MemoryStorageWithLatency.cs | 5 ++++- .../TestExtensions/MockStorageProvider.cs | 2 ++ .../StorageProviders/BaseJSONStorageProvider.cs | 3 +++ 11 files changed, 56 insertions(+), 3 deletions(-) diff --git a/Orleans.sln b/Orleans.sln index 630638a0ac..083be67e50 100644 --- a/Orleans.sln +++ b/Orleans.sln @@ -218,7 +218,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DistributedTests.Server", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Orleans.Persistence.Migration", "src\Orleans.Persistence.Migration\Orleans.Persistence.Migration.csproj", "{E0B94181-200A-4CC6-94CA-D3D81CA6720A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Migration.Tests", "test\Extensions\Migration.Tests\Migration.Tests.csproj", "{57285DA6-6EF1-4B46-A178-1ABB48CD0B57}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Migration.Tests", "test\Extensions\Migration.Tests\Migration.Tests.csproj", "{57285DA6-6EF1-4B46-A178-1ABB48CD0B57}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Orleans.Persistence.AzureStorage.Migration", "src\Azure\Orleans.Persistence.AzureStorage.Migration\Orleans.Persistence.AzureStorage.Migration.csproj", "{0073C9D9-1906-452A-873E-9A2639FFB041}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -1346,6 +1348,18 @@ Global {57285DA6-6EF1-4B46-A178-1ABB48CD0B57}.Release|x64.Build.0 = Release|Any CPU {57285DA6-6EF1-4B46-A178-1ABB48CD0B57}.Release|x86.ActiveCfg = Release|Any CPU {57285DA6-6EF1-4B46-A178-1ABB48CD0B57}.Release|x86.Build.0 = Release|Any CPU + {0073C9D9-1906-452A-873E-9A2639FFB041}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0073C9D9-1906-452A-873E-9A2639FFB041}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0073C9D9-1906-452A-873E-9A2639FFB041}.Debug|x64.ActiveCfg = Debug|Any CPU + {0073C9D9-1906-452A-873E-9A2639FFB041}.Debug|x64.Build.0 = Debug|Any CPU + {0073C9D9-1906-452A-873E-9A2639FFB041}.Debug|x86.ActiveCfg = Debug|Any CPU + {0073C9D9-1906-452A-873E-9A2639FFB041}.Debug|x86.Build.0 = Debug|Any CPU + {0073C9D9-1906-452A-873E-9A2639FFB041}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0073C9D9-1906-452A-873E-9A2639FFB041}.Release|Any CPU.Build.0 = Release|Any CPU + {0073C9D9-1906-452A-873E-9A2639FFB041}.Release|x64.ActiveCfg = Release|Any CPU + {0073C9D9-1906-452A-873E-9A2639FFB041}.Release|x64.Build.0 = Release|Any CPU + {0073C9D9-1906-452A-873E-9A2639FFB041}.Release|x86.ActiveCfg = Release|Any CPU + {0073C9D9-1906-452A-873E-9A2639FFB041}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1457,6 +1471,7 @@ Global {E8335DC9-9A7F-45C1-AFA3-0AA93ABD4FA5} = {FFEC9FEE-FEDF-4510-B7D2-0B0B3374ED2F} {E0B94181-200A-4CC6-94CA-D3D81CA6720A} = {FE2E08C6-9C3B-4AEE-AE07-CCA387580D7A} {57285DA6-6EF1-4B46-A178-1ABB48CD0B57} = {082D25DB-70CA-48F4-93E0-EC3455F494B8} + {0073C9D9-1906-452A-873E-9A2639FFB041} = {4C5D66BF-EE1C-4DD8-8551-D1B7F3768A34} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7BFB3429-B5BB-4DB1-95B4-67D77A864952} diff --git a/src/AWS/Orleans.Persistence.DynamoDB/Provider/DynamoDBGrainStorage.cs b/src/AWS/Orleans.Persistence.DynamoDB/Provider/DynamoDBGrainStorage.cs index fd843d017e..4f4fc57c47 100755 --- a/src/AWS/Orleans.Persistence.DynamoDB/Provider/DynamoDBGrainStorage.cs +++ b/src/AWS/Orleans.Persistence.DynamoDB/Provider/DynamoDBGrainStorage.cs @@ -384,6 +384,8 @@ internal void ConvertToStorageFormat(object grainState, GrainStateRecord entity) throw new ArgumentOutOfRangeException("GrainState.Size", msg); } } + + public Task> GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); } public static class DynamoDBGrainStorageFactory diff --git a/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/AdoNetGrainStorage.cs b/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/AdoNetGrainStorage.cs index 3e23a9c66f..5878ba7373 100644 --- a/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/AdoNetGrainStorage.cs +++ b/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/AdoNetGrainStorage.cs @@ -614,5 +614,7 @@ private ICollection ConfigureSerializers(AdoNetGrainStorageO return serializers; } + + public Task> GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); } } \ No newline at end of file diff --git a/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureBlobStorage.cs b/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureBlobStorage.cs index 05f8ac02a6..8c952bf798 100644 --- a/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureBlobStorage.cs +++ b/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureBlobStorage.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Azure; @@ -305,6 +307,8 @@ private object ConvertFromStorageFormat(byte[] contents, Type stateType) return result; } + + public async Task> GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); } public static class AzureBlobGrainStorageFactory diff --git a/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureTableStorage.cs b/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureTableStorage.cs index 4b1a939445..8215ef0eb8 100644 --- a/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureTableStorage.cs +++ b/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureTableStorage.cs @@ -496,6 +496,8 @@ public void Participate(ISiloLifecycle lifecycle) { lifecycle.Subscribe(OptionFormattingUtilities.Name(this.name), this.options.InitStage, Init, Close); } + + public Task> GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); } public static class AzureTableGrainStorageFactory diff --git a/src/Orleans.Core/Providers/IGrainStorage.cs b/src/Orleans.Core/Providers/IGrainStorage.cs index a1995084ae..b96cbbf67b 100644 --- a/src/Orleans.Core/Providers/IGrainStorage.cs +++ b/src/Orleans.Core/Providers/IGrainStorage.cs @@ -1,11 +1,20 @@ -using System; +using System; using System.Runtime.Serialization; using System.Threading.Tasks; using Orleans.Runtime; using System.Net; +using System.Collections.Generic; +using System.Threading; namespace Orleans.Storage { + public readonly struct StorageEntry + { + public string Name { get; } + public GrainReference GrainReference { get; } + public IGrainState GrainState { get; } + } + /// /// Interface to be implemented for a storage able to read and write Orleans grain state data. /// @@ -31,6 +40,12 @@ public interface IGrainStorage /// Copy of last-known state data object for this grain. /// Completion promise for the Delete operation on the specified grain. Task ClearStateAsync(string grainType, GrainReference grainReference, IGrainState grainState); + + /// + /// Get all entries in storage + /// + /// The entries in storage + Task> GetAll(CancellationToken cancellationToken); } /// diff --git a/src/Orleans.TestingHost/TestStorageProviders/FaultInjectionStorageProvider.cs b/src/Orleans.TestingHost/TestStorageProviders/FaultInjectionStorageProvider.cs index 89de8eff69..e78b98a03c 100644 --- a/src/Orleans.TestingHost/TestStorageProviders/FaultInjectionStorageProvider.cs +++ b/src/Orleans.TestingHost/TestStorageProviders/FaultInjectionStorageProvider.cs @@ -8,6 +8,7 @@ using Orleans.Storage; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using System.Collections.Generic; namespace Orleans.TestingHost { @@ -119,6 +120,8 @@ public void Participate(ISiloLifecycle lifecycle) { (realStorageProvider as ILifecycleParticipant)?.Participate(lifecycle); } + + public Task> GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); } /// diff --git a/src/OrleansProviders/Storage/MemoryStorage.cs b/src/OrleansProviders/Storage/MemoryStorage.cs index c399906773..85caee41e1 100644 --- a/src/OrleansProviders/Storage/MemoryStorage.cs +++ b/src/OrleansProviders/Storage/MemoryStorage.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -132,6 +133,7 @@ private IMemoryStorageGrain GetStorageGrain(string id) } public void Dispose() => storageGrains = null; + public Task> GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); } /// diff --git a/src/OrleansProviders/Storage/MemoryStorageWithLatency.cs b/src/OrleansProviders/Storage/MemoryStorageWithLatency.cs index 188132a138..94dde2473e 100644 --- a/src/OrleansProviders/Storage/MemoryStorageWithLatency.cs +++ b/src/OrleansProviders/Storage/MemoryStorageWithLatency.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Orleans.Configuration; +using System.Collections.Generic; namespace Orleans.Storage { @@ -39,7 +40,7 @@ public class MemoryStorageWithLatencyOptions : MemoryGrainStorageOptions /// /// [DebuggerDisplay("MemoryStore:{Name},WithLatency:{latency}")] - public class MemoryGrainStorageWithLatency :IGrainStorage + public class MemoryGrainStorageWithLatency : IGrainStorage { private const int NUM_STORE_GRAINS = 1; private MemoryGrainStorage baseGranStorage; @@ -111,5 +112,7 @@ private async Task MakeFixedLatencyCall(Func action) throw new AggregateException(error); } } + + public Task> GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); } } diff --git a/test/TestInfrastructure/TestExtensions/MockStorageProvider.cs b/test/TestInfrastructure/TestExtensions/MockStorageProvider.cs index b3057cdc8c..75a8cc7bb3 100644 --- a/test/TestInfrastructure/TestExtensions/MockStorageProvider.cs +++ b/test/TestInfrastructure/TestExtensions/MockStorageProvider.cs @@ -278,5 +278,7 @@ public virtual Task ExecuteCommand(int command, object arg) return Task.FromResult(true); } } + + public Task> GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); } } diff --git a/test/TesterInternal/StorageTests/StorageProviders/BaseJSONStorageProvider.cs b/test/TesterInternal/StorageTests/StorageProviders/BaseJSONStorageProvider.cs index 49dc274479..1ebfa7e636 100644 --- a/test/TesterInternal/StorageTests/StorageProviders/BaseJSONStorageProvider.cs +++ b/test/TesterInternal/StorageTests/StorageProviders/BaseJSONStorageProvider.cs @@ -15,6 +15,7 @@ //********************************************************* using System; +using System.Collections.Generic; using System.Threading.Tasks; using Newtonsoft.Json; using Orleans; @@ -131,6 +132,8 @@ protected static void ConvertFromStorageFormat(IGrainState grainState, string en object data = JsonConvert.DeserializeObject(entityData, grainState.State.GetType()); grainState.State = data; } + + public Task> GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); } } From 5da476b493882b5e4056083edcc604e5cdeb2bea Mon Sep 17 00:00:00 2001 From: Benjamin Petit Date: Wed, 7 Dec 2022 16:29:14 +0100 Subject: [PATCH 3/5] WIP --- .../Provider/DynamoDBGrainStorage.cs | 2 +- .../Storage/Provider/AdoNetGrainStorage.cs | 3 +- .../AzureBlobStorage.cs | 362 ++++++++++++++++++ ....Persistence.AzureStorage.Migration.csproj | 21 + .../Properties/AssemblyInfo.cs | 1 + .../Providers/Storage/AzureBlobStorage.cs | 21 +- .../Providers/Storage/AzureTableStorage.cs | 3 +- .../Runtime/GrainReference.cs | 2 +- src/Orleans.Core/Providers/IGrainStorage.cs | 10 +- .../Orleans.Persistence.Migration.csproj | 4 + .../Serialization/IGrainStorageSerializer.cs | 86 +++++ .../JsonGrainStorageSerializer.cs | 39 ++ .../FaultInjectionStorageProvider.cs | 3 +- src/OrleansProviders/Storage/MemoryStorage.cs | 3 +- .../Storage/MemoryStorageWithLatency.cs | 2 +- .../TestExtensions/MockStorageProvider.cs | 2 +- .../BaseJSONStorageProvider.cs | 3 +- 17 files changed, 556 insertions(+), 11 deletions(-) create mode 100644 src/Azure/Orleans.Persistence.AzureStorage.Migration/AzureBlobStorage.cs create mode 100644 src/Azure/Orleans.Persistence.AzureStorage.Migration/Orleans.Persistence.AzureStorage.Migration.csproj create mode 100644 src/Orleans.Persistence.Migration/Serialization/IGrainStorageSerializer.cs create mode 100644 src/Orleans.Persistence.Migration/Serialization/JsonGrainStorageSerializer.cs diff --git a/src/AWS/Orleans.Persistence.DynamoDB/Provider/DynamoDBGrainStorage.cs b/src/AWS/Orleans.Persistence.DynamoDB/Provider/DynamoDBGrainStorage.cs index 4f4fc57c47..f17eb4768a 100755 --- a/src/AWS/Orleans.Persistence.DynamoDB/Provider/DynamoDBGrainStorage.cs +++ b/src/AWS/Orleans.Persistence.DynamoDB/Provider/DynamoDBGrainStorage.cs @@ -385,7 +385,7 @@ internal void ConvertToStorageFormat(object grainState, GrainStateRecord entity) } } - public Task> GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); + public IAsyncEnumerable GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); } public static class DynamoDBGrainStorageFactory diff --git a/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/AdoNetGrainStorage.cs b/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/AdoNetGrainStorage.cs index 5878ba7373..5a8d0732d8 100644 --- a/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/AdoNetGrainStorage.cs +++ b/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/AdoNetGrainStorage.cs @@ -18,6 +18,7 @@ using Orleans.Configuration; using Orleans.Configuration.Overrides; using Orleans.Runtime.Configuration; +using System.Runtime.CompilerServices; namespace Orleans.Storage { @@ -615,6 +616,6 @@ private ICollection ConfigureSerializers(AdoNetGrainStorageO return serializers; } - public Task> GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); + public IAsyncEnumerable GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); } } \ No newline at end of file diff --git a/src/Azure/Orleans.Persistence.AzureStorage.Migration/AzureBlobStorage.cs b/src/Azure/Orleans.Persistence.AzureStorage.Migration/AzureBlobStorage.cs new file mode 100644 index 0000000000..ab953ec717 --- /dev/null +++ b/src/Azure/Orleans.Persistence.AzureStorage.Migration/AzureBlobStorage.cs @@ -0,0 +1,362 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Azure; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Orleans.Configuration; +using Orleans.Persistence.Migration; +using Orleans.Providers.Azure; +using Orleans.Runtime; +using Orleans.Serialization; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace Orleans.Storage.Migration.AzureStorage +{ + /// + /// Simple storage provider for writing grain state data to Azure blob storage in JSON format. + /// + public class MigrationAzureBlobGrainStorage : IGrainStorage, ILifecycleParticipant + { + private BlobContainerClient container; + private ILogger logger; + private readonly string name; + private AzureBlobStorageOptions options; + private IGrainStorageSerializer grainStorageSerializer; + private readonly IGrainReferenceExtractor grainReferenceExtractor; + private readonly IServiceProvider services; + + /// Default constructor + public MigrationAzureBlobGrainStorage( + string name, + AzureBlobStorageOptions options, + IGrainStorageSerializer grainStorageSerializer, + IGrainReferenceExtractor grainReferenceExtractor, + IServiceProvider services, + ILogger logger) + { + this.name = name; + this.options = options; + this.grainStorageSerializer = grainStorageSerializer; + this.grainReferenceExtractor = grainReferenceExtractor; + this.services = services; + this.logger = logger; + } + + /// Read state data function for this storage provider. + /// + public async Task ReadStateAsync(string grainType, GrainReference grainReference, IGrainState grainState) + { + var grainId = this.grainReferenceExtractor.GetGrainId(grainReference); + var blobName = GetBlobName(grainType, grainId); + if (this.logger.IsEnabled(LogLevel.Trace)) this.logger.LogTrace((int)AzureProviderErrorCode.AzureBlobProvider_Storage_Reading, + "Reading: GrainType={GrainType} Grainid={GrainId} ETag={ETag} from BlobName={BlobName} in Container={ContainerName}", + grainType, + grainId, + grainState.ETag, + blobName, + container.Name); + + try + { + var blob = container.GetBlobClient(blobName); + + BinaryData contents; + try + { + var response = await blob.DownloadContentAsync(); + grainState.ETag = response.Value.Details.ETag.ToString(); + contents = response.Value.Content; + } + catch (RequestFailedException exception) when (exception.IsBlobNotFound()) + { + if (this.logger.IsEnabled(LogLevel.Trace)) this.logger.LogTrace((int)AzureProviderErrorCode.AzureBlobProvider_BlobNotFound, + "BlobNotFound reading: GrainType={GrainType} Grainid={GrainId} ETag={ETag} from BlobName={BlobName} in Container={ContainerName}", + grainType, + grainId, + grainState.ETag, + blobName, + container.Name); + return; + } + catch (RequestFailedException exception) when (exception.IsContainerNotFound()) + { + if (this.logger.IsEnabled(LogLevel.Trace)) this.logger.LogTrace((int)AzureProviderErrorCode.AzureBlobProvider_ContainerNotFound, + "ContainerNotFound reading: GrainType={GrainType} Grainid={GrainId} ETag={ETag} from BlobName={BlobName} in Container={ContainerName}", + grainType, + grainId, + grainState.ETag, + blobName, + container.Name); + return; + } + + if (contents == null) // TODO bpetit + { + if (this.logger.IsEnabled(LogLevel.Trace)) this.logger.LogTrace((int)AzureProviderErrorCode.AzureBlobProvider_BlobEmpty, + "BlobEmpty reading: GrainType={GrainType} Grainid={GrainId} ETag={ETag} from BlobName={BlobName} in Container={ContainerName}", + grainType, + grainId, + grainState.ETag, + blobName, + container.Name); + grainState.RecordExists = false; + return; + } + else + { + grainState.RecordExists = true; + } + + var loadedState = this.ConvertFromStorageFormat(contents); + grainState.State = loadedState ?? Activator.CreateInstance(grainState.Type); + + if (this.logger.IsEnabled(LogLevel.Trace)) this.logger.LogTrace((int)AzureProviderErrorCode.AzureBlobProvider_Storage_DataRead, + "Read: GrainType={GrainType} Grainid={GrainId} ETag={ETag} from BlobName={BlobName} in Container={ContainerName}", + grainType, + grainId, + grainState.ETag, + blobName, + container.Name); + } + catch (Exception ex) + { + logger.LogError((int)AzureProviderErrorCode.AzureBlobProvider_ReadError, + ex, + "Error reading: GrainType={GrainType} Grainid={GrainId} ETag={ETag} from BlobName={BlobName} in Container={ContainerName}", + grainType, + grainId, + grainState.ETag, + blobName, + container.Name); + + throw; + } + } + + private static string GetBlobName(string grainType, string grainId) => $"{grainType}-{grainId}.json"; + + /// Write state data function for this storage provider. + /// + public async Task WriteStateAsync(string grainType, GrainReference grainReference, IGrainState grainState) + { + var grainId = this.grainReferenceExtractor.GetGrainId(grainReference); + var blobName = GetBlobName(grainType, grainId); + try + { + if (this.logger.IsEnabled(LogLevel.Trace)) this.logger.LogTrace((int)AzureProviderErrorCode.AzureBlobProvider_Storage_Writing, + "Writing: GrainType={GrainType} GrainId={GrainId} ETag={ETag} to BlobName={BlobName} in Container={ContainerName}", + grainType, + grainId, + grainState.ETag, + blobName, + container.Name); + + var contents = ConvertToStorageFormat(grainState.State); + + var blob = container.GetBlobClient(blobName); + + await WriteStateAndCreateContainerIfNotExists(grainType, grainId, grainState, contents, "application/octet-stream", blob); + + if (this.logger.IsEnabled(LogLevel.Trace)) this.logger.LogTrace((int)AzureProviderErrorCode.AzureBlobProvider_Storage_DataRead, + "Written: GrainType={GrainType} GrainId={GrainId} ETag={ETag} to BlobName={BlobName} in Container={ContainerName}", + grainType, + grainId, + grainState.ETag, + blobName, + container.Name); + } + catch (Exception ex) + { + logger.LogError((int)AzureProviderErrorCode.AzureBlobProvider_WriteError, + ex, + "Error writing: GrainType={GrainType} GrainId={GrainId} ETag={ETag} to BlobName={BlobName} in Container={ContainerName}", + grainType, + grainId, + grainState.ETag, + blobName, + container.Name); + + throw; + } + } + + /// Clear / Delete state data function for this storage provider. + /// + public async Task ClearStateAsync(string grainType, GrainReference grainReference, IGrainState grainState) + { + var grainId = this.grainReferenceExtractor.GetGrainId(grainReference); + var blobName = GetBlobName(grainType, grainId); + try + { + if (this.logger.IsEnabled(LogLevel.Trace)) this.logger.LogTrace((int)AzureProviderErrorCode.AzureBlobProvider_ClearingData, + "Clearing: GrainType={GrainType} GrainId={GrainId} ETag={ETag} BlobName={BlobName} in Container={ContainerName}", + grainType, + grainId, + grainState.ETag, + blobName, + container.Name); + + var blob = container.GetBlobClient(blobName); + + var conditions = string.IsNullOrEmpty(grainState.ETag) + ? new BlobRequestConditions { IfNoneMatch = ETag.All } + : new BlobRequestConditions { IfMatch = new ETag(grainState.ETag) }; + + await DoOptimisticUpdate(() => blob.DeleteIfExistsAsync(DeleteSnapshotsOption.None, conditions: conditions), + blob, grainState.ETag).ConfigureAwait(false); + + grainState.ETag = null; + grainState.RecordExists = false; + + if (this.logger.IsEnabled(LogLevel.Trace)) + { + var properties = await blob.GetPropertiesAsync(); + this.logger.LogTrace((int)AzureProviderErrorCode.AzureBlobProvider_Cleared, + "Cleared: GrainType={GrainType} GrainId={GrainId} ETag={ETag} BlobName={BlobName} in Container={ContainerName}", + grainType, + grainId, + properties.Value.ETag, + blobName, + container.Name); + } + } + catch (Exception ex) + { + logger.LogError((int)AzureProviderErrorCode.AzureBlobProvider_ClearError, + ex, + "Error clearing: GrainType={GrainType} GrainId={GrainId} ETag={ETag} BlobName={BlobName} in Container={ContainerName}", + grainType, + grainId, + grainState.ETag, + blobName, + container.Name); + + throw; + } + } + + private async Task WriteStateAndCreateContainerIfNotExists(string grainType, string grainId, IGrainState grainState, BinaryData contents, string mimeType, BlobClient blob) + { + try + { + var conditions = string.IsNullOrEmpty(grainState.ETag) + ? new BlobRequestConditions { IfNoneMatch = ETag.All } + : new BlobRequestConditions { IfMatch = new ETag(grainState.ETag) }; + + var options = new BlobUploadOptions + { + HttpHeaders = new BlobHttpHeaders { ContentType = mimeType }, + Conditions = conditions, + }; + + var result = await DoOptimisticUpdate( + () => blob.UploadAsync(contents, options), + blob, + grainState.ETag) + .ConfigureAwait(false); + + grainState.ETag = result.Value.ETag.ToString(); + grainState.RecordExists = true; + } + catch (RequestFailedException exception) when (exception.IsContainerNotFound()) + { + // if the container does not exist, create it, and make another attempt + if (this.logger.IsEnabled(LogLevel.Trace)) this.logger.LogTrace((int)AzureProviderErrorCode.AzureBlobProvider_ContainerNotFound, + "Creating container: GrainType={GrainType} GrainId={GrainId} ETag={ETag} to BlobName={BlobName} in Container={ContainerName}", + grainType, + grainId, + grainState.ETag, + blob.Name, + container.Name); + await container.CreateIfNotExistsAsync().ConfigureAwait(false); + + await WriteStateAndCreateContainerIfNotExists(grainType, grainId, grainState, contents, mimeType, blob).ConfigureAwait(false); + } + } + + private static async Task DoOptimisticUpdate(Func> updateOperation, BlobClient blob, string currentETag) + { + try + { + return await updateOperation.Invoke().ConfigureAwait(false); + } + catch (RequestFailedException ex) when (ex.IsPreconditionFailed() || ex.IsConflict()) + { + throw new InconsistentStateException($"Blob storage condition not Satisfied. BlobName: {blob.Name}, Container: {blob.BlobContainerName}, CurrentETag: {currentETag}", "Unknown", currentETag, ex); + } + } + + public void Participate(ISiloLifecycle lifecycle) + { + lifecycle.Subscribe(OptionFormattingUtilities.Name(this.name), this.options.InitStage, Init); + } + + /// Initialization function for this storage provider. + private async Task Init(CancellationToken ct) + { + var stopWatch = Stopwatch.StartNew(); + + try + { + this.logger.LogInformation((int)AzureProviderErrorCode.AzureTableProvider_InitProvider, "AzureBlobGrainStorage initializing: {Options}", this.options.ToString()); + + if (options.CreateClient is not { } createClient) + { + throw new OrleansConfigurationException($"No credentials specified. Use the {options.GetType().Name}.{nameof(AzureBlobStorageOptions.ConfigureBlobServiceClient)} method to configure the Azure Blob Service client."); + } + + var client = await createClient(); + container = client.GetBlobContainerClient(this.options.ContainerName); + await container.CreateIfNotExistsAsync().ConfigureAwait(false); + stopWatch.Stop(); + this.logger.LogInformation((int)AzureProviderErrorCode.AzureBlobProvider_InitProvider, + "Initializing provider {ProviderName} of type {ProviderType} in stage {Stage} took {ElapsedMilliseconds} Milliseconds.", + this.name, + this.GetType().Name, + this.options.InitStage, + stopWatch.ElapsedMilliseconds); + } + catch (Exception ex) + { + stopWatch.Stop(); + this.logger.LogError((int)ErrorCode.Provider_ErrorFromInit, + ex, + "Initialization failed for provider {ProviderName} of type {ProviderType} in stage {Stage} in {ElapsedMilliseconds} Milliseconds.", + this.name, + this.GetType().Name, + this.options.InitStage, + stopWatch.ElapsedMilliseconds); + throw; + } + } + + /// + /// Serialize to the configured storage format + /// + /// The grain state data to be serialized + private BinaryData ConvertToStorageFormat(T grainState) => this.grainStorageSerializer.Serialize(grainState); + + /// + /// Deserialize from the configured storage format + /// + /// The serialized contents. + private object ConvertFromStorageFormat(BinaryData contents) => this.grainStorageSerializer.Deserialize(contents); + public IAsyncEnumerable GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); + } + + public static class AzureBlobGrainStorageFactory + { + public static IGrainStorage Create(IServiceProvider services, string name) + { + var optionsMonitor = services.GetRequiredService>(); + return ActivatorUtilities.CreateInstance(services, name, optionsMonitor.Get(name)); + } + } +} diff --git a/src/Azure/Orleans.Persistence.AzureStorage.Migration/Orleans.Persistence.AzureStorage.Migration.csproj b/src/Azure/Orleans.Persistence.AzureStorage.Migration/Orleans.Persistence.AzureStorage.Migration.csproj new file mode 100644 index 0000000000..77c40d80a8 --- /dev/null +++ b/src/Azure/Orleans.Persistence.AzureStorage.Migration/Orleans.Persistence.AzureStorage.Migration.csproj @@ -0,0 +1,21 @@ + + + + net7.0 + enable + disable + + + + + + + + + + + + + + + diff --git a/src/Azure/Orleans.Persistence.AzureStorage/Properties/AssemblyInfo.cs b/src/Azure/Orleans.Persistence.AzureStorage/Properties/AssemblyInfo.cs index 3a3d99f3cc..b7b261c244 100644 --- a/src/Azure/Orleans.Persistence.AzureStorage/Properties/AssemblyInfo.cs +++ b/src/Azure/Orleans.Persistence.AzureStorage/Properties/AssemblyInfo.cs @@ -1,3 +1,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Tester.AzureUtils")] +[assembly: InternalsVisibleTo("Orleans.Persistence.AzureStorage.Migration")] diff --git a/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureBlobStorage.cs b/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureBlobStorage.cs index 8c952bf798..6b7e5bc5cc 100644 --- a/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureBlobStorage.cs +++ b/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureBlobStorage.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; using System.Threading; @@ -37,6 +38,7 @@ public class AzureBlobGrainStorage : IGrainStorage, ILifecycleParticipant Default constructor public AzureBlobGrainStorage( @@ -45,6 +47,7 @@ public AzureBlobGrainStorage( SerializationManager serializationManager, IGrainFactory grainFactory, ITypeResolver typeResolver, + IGrainReferenceRuntime grainReferenceRuntime, ILogger logger) { this.name = name; @@ -52,6 +55,7 @@ public AzureBlobGrainStorage( this.serializationManager = serializationManager; this.grainFactory = grainFactory; this.typeResolver = typeResolver; + this.grainReferenceRuntime = grainReferenceRuntime; this.logger = logger; } @@ -308,7 +312,22 @@ private object ConvertFromStorageFormat(byte[] contents, Type stateType) return result; } - public async Task> GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); + public async IAsyncEnumerable GetAll([EnumeratorCancellation] CancellationToken cancellationToken) + { + var regex = new Regex("(?\\w+)-(?\\w+).json"); + await foreach (var item in this.container.GetBlobsAsync(cancellationToken: cancellationToken)) + { + var match = regex.Match(item.Name); + if (match.Success) + { + var name = match.Groups["name"].Value; + var reference = GrainReference.FromKeyString(match.Groups["reference"].Value, this.grainReferenceRuntime); + var state = new GrainState(); + await ReadStateAsync(name, reference, state); + yield return new StorageEntry(name, reference, state); + } + } + } } public static class AzureBlobGrainStorageFactory diff --git a/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureTableStorage.cs b/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureTableStorage.cs index 8215ef0eb8..e0853880f0 100644 --- a/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureTableStorage.cs +++ b/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureTableStorage.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Linq; using System.Net; +using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -497,7 +498,7 @@ public void Participate(ISiloLifecycle lifecycle) lifecycle.Subscribe(OptionFormattingUtilities.Name(this.name), this.options.InitStage, Init, Close); } - public Task> GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); + public IAsyncEnumerable GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); } public static class AzureTableGrainStorageFactory diff --git a/src/Orleans.Core.Abstractions/Runtime/GrainReference.cs b/src/Orleans.Core.Abstractions/Runtime/GrainReference.cs index 3e4a5ff8d5..297e010ed2 100644 --- a/src/Orleans.Core.Abstractions/Runtime/GrainReference.cs +++ b/src/Orleans.Core.Abstractions/Runtime/GrainReference.cs @@ -413,7 +413,7 @@ public GrainReferenceKeyInfo ToKeyInfo() return new GrainReferenceKeyInfo(GrainId.ToKeyInfo()); } - internal static GrainReference FromKeyString(string key, IGrainReferenceRuntime runtime) + public static GrainReference FromKeyString(string key, IGrainReferenceRuntime runtime) { if (string.IsNullOrWhiteSpace(key)) throw new ArgumentNullException(nameof(key), "GrainReference.FromKeyString cannot parse null key"); diff --git a/src/Orleans.Core/Providers/IGrainStorage.cs b/src/Orleans.Core/Providers/IGrainStorage.cs index b96cbbf67b..d20e8c8c5b 100644 --- a/src/Orleans.Core/Providers/IGrainStorage.cs +++ b/src/Orleans.Core/Providers/IGrainStorage.cs @@ -5,6 +5,7 @@ using System.Net; using System.Collections.Generic; using System.Threading; +using System.Runtime.CompilerServices; namespace Orleans.Storage { @@ -13,6 +14,13 @@ public readonly struct StorageEntry public string Name { get; } public GrainReference GrainReference { get; } public IGrainState GrainState { get; } + + public StorageEntry(string name, GrainReference grainReference, IGrainState grainState) + { + this.Name = name; + this.GrainReference = grainReference; + this.GrainState = grainState; + } } /// @@ -45,7 +53,7 @@ public interface IGrainStorage /// Get all entries in storage /// /// The entries in storage - Task> GetAll(CancellationToken cancellationToken); + IAsyncEnumerable GetAll(CancellationToken cancellationToken); } /// diff --git a/src/Orleans.Persistence.Migration/Orleans.Persistence.Migration.csproj b/src/Orleans.Persistence.Migration/Orleans.Persistence.Migration.csproj index 9930394fca..a6aaca264e 100644 --- a/src/Orleans.Persistence.Migration/Orleans.Persistence.Migration.csproj +++ b/src/Orleans.Persistence.Migration/Orleans.Persistence.Migration.csproj @@ -17,4 +17,8 @@ + + + + diff --git a/src/Orleans.Persistence.Migration/Serialization/IGrainStorageSerializer.cs b/src/Orleans.Persistence.Migration/Serialization/IGrainStorageSerializer.cs new file mode 100644 index 0000000000..3d139936f6 --- /dev/null +++ b/src/Orleans.Persistence.Migration/Serialization/IGrainStorageSerializer.cs @@ -0,0 +1,86 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Orleans.Runtime; + +namespace Orleans.Storage +{ + /// + /// Common interface for grain state serializers. + /// + public interface IGrainStorageSerializer + { + /// + /// Serializes the object input. + /// + /// The object to serialize. + /// The input type. + /// The serialized input. + BinaryData Serialize(T input); + + /// + /// Deserializes the provided data. + /// + /// The data to deserialize. + /// The output type. + /// The deserialized object. + T Deserialize(BinaryData input); + } + + /// + /// Extensions for . + /// + public static class GrainStorageSerializerExtensions + { + /// + /// Deserializes the provided data. + /// + /// The grain state serializer. + /// The data to deserialize. + /// The output type. + /// The deserialized object. + public static T Deserialize(this IGrainStorageSerializer serializer, ReadOnlyMemory input) + => serializer.Deserialize(new BinaryData(input)); + } + + /// + /// Interface to be implemented by the storage provider options. + /// + public interface IStorageProviderSerializerOptions + { + /// + /// Gets or sets the serializer to use for this storage provider. + /// + IGrainStorageSerializer GrainStorageSerializer { get; set; } + } + + /// + /// Provides default configuration for . + /// + /// The options type. + public class DefaultStorageProviderSerializerOptionsConfigurator : IPostConfigureOptions where TOptions : class, IStorageProviderSerializerOptions + { + private readonly IServiceProvider _serviceProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The service provider. + public DefaultStorageProviderSerializerOptionsConfigurator(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + /// + public void PostConfigure(string name, TOptions options) + { + if (options.GrainStorageSerializer == default) + { + // First, try to get a IGrainStorageSerializer that was registered with + // the same name as the storage provider + // If none is found, fallback to system wide default + options.GrainStorageSerializer = _serviceProvider.GetServiceByName(name) ?? _serviceProvider.GetRequiredService(); + } + } + } +} diff --git a/src/Orleans.Persistence.Migration/Serialization/JsonGrainStorageSerializer.cs b/src/Orleans.Persistence.Migration/Serialization/JsonGrainStorageSerializer.cs new file mode 100644 index 0000000000..00bef6f73b --- /dev/null +++ b/src/Orleans.Persistence.Migration/Serialization/JsonGrainStorageSerializer.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Orleans.Persistence.Migration.Serialization; +using Orleans.Serialization; + +namespace Orleans.Storage +{ + /// + /// Grain storage serializer that uses Newtonsoft.Json + /// + public class JsonGrainStorageSerializer : IGrainStorageSerializer + { + private readonly OrleansMigrationJsonSerializer _orleansJsonSerializer; + + /// + /// Initializes a new instance of the class. + /// + public JsonGrainStorageSerializer(OrleansMigrationJsonSerializer orleansJsonSerializer) + { + _orleansJsonSerializer = orleansJsonSerializer; + } + + /// + public BinaryData Serialize(T value) + { + var data = _orleansJsonSerializer.Serialize(value, typeof(T)); + return new BinaryData(data); + } + + /// + public T Deserialize(BinaryData input) + { + return (T)_orleansJsonSerializer.Deserialize(typeof(T), input.ToString()); + } + } +} diff --git a/src/Orleans.TestingHost/TestStorageProviders/FaultInjectionStorageProvider.cs b/src/Orleans.TestingHost/TestStorageProviders/FaultInjectionStorageProvider.cs index e78b98a03c..33bc4bea3c 100644 --- a/src/Orleans.TestingHost/TestStorageProviders/FaultInjectionStorageProvider.cs +++ b/src/Orleans.TestingHost/TestStorageProviders/FaultInjectionStorageProvider.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using System.Collections.Generic; +using System.Threading; namespace Orleans.TestingHost { @@ -121,7 +122,7 @@ public void Participate(ISiloLifecycle lifecycle) (realStorageProvider as ILifecycleParticipant)?.Participate(lifecycle); } - public Task> GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); + public IAsyncEnumerable GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); } /// diff --git a/src/OrleansProviders/Storage/MemoryStorage.cs b/src/OrleansProviders/Storage/MemoryStorage.cs index 85caee41e1..f6c39681c1 100644 --- a/src/OrleansProviders/Storage/MemoryStorage.cs +++ b/src/OrleansProviders/Storage/MemoryStorage.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -133,7 +134,7 @@ private IMemoryStorageGrain GetStorageGrain(string id) } public void Dispose() => storageGrains = null; - public Task> GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); + public IAsyncEnumerable GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); } /// diff --git a/src/OrleansProviders/Storage/MemoryStorageWithLatency.cs b/src/OrleansProviders/Storage/MemoryStorageWithLatency.cs index 94dde2473e..5ce65f3eee 100644 --- a/src/OrleansProviders/Storage/MemoryStorageWithLatency.cs +++ b/src/OrleansProviders/Storage/MemoryStorageWithLatency.cs @@ -113,6 +113,6 @@ private async Task MakeFixedLatencyCall(Func action) } } - public Task> GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); + public IAsyncEnumerable GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); } } diff --git a/test/TestInfrastructure/TestExtensions/MockStorageProvider.cs b/test/TestInfrastructure/TestExtensions/MockStorageProvider.cs index 75a8cc7bb3..b56685104a 100644 --- a/test/TestInfrastructure/TestExtensions/MockStorageProvider.cs +++ b/test/TestInfrastructure/TestExtensions/MockStorageProvider.cs @@ -279,6 +279,6 @@ public virtual Task ExecuteCommand(int command, object arg) } } - public Task> GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); + public IAsyncEnumerable GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); } } diff --git a/test/TesterInternal/StorageTests/StorageProviders/BaseJSONStorageProvider.cs b/test/TesterInternal/StorageTests/StorageProviders/BaseJSONStorageProvider.cs index 1ebfa7e636..51f7293485 100644 --- a/test/TesterInternal/StorageTests/StorageProviders/BaseJSONStorageProvider.cs +++ b/test/TesterInternal/StorageTests/StorageProviders/BaseJSONStorageProvider.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; using Orleans; @@ -133,7 +134,7 @@ protected static void ConvertFromStorageFormat(IGrainState grainState, string en grainState.State = data; } - public Task> GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); + public IAsyncEnumerable GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); } } From e7c4ebf8776f49404609786d8ae82841e9d45676 Mon Sep 17 00:00:00 2001 From: Benjamin Petit Date: Fri, 9 Dec 2022 17:54:23 +0100 Subject: [PATCH 4/5] WIP AzureBlob migration provider --- Orleans.sln | 15 ++ .../AzureBlobSiloBuilderExtensions.cs | 121 +++++++++++++ ...e.cs => MigrationAzureBlobGrainStorage.cs} | 2 +- .../Providers/Storage/AzureBlobStorage.cs | 2 +- .../GrainReferenceExtractor.cs | 37 ++-- .../HostingExtensions.cs | 78 ++++++++- .../MigrationGrainStorage.cs | 104 +++++++++++ .../MigrationAzureBlobTests.cs | 49 ++++++ .../MigrationBaseTests.cs | 163 ++++++++++++++++++ .../Tester.AzureUtils.Migration.csproj | 26 +++ .../TestDataSets/TestStateGeneric1.cs | 2 +- 11 files changed, 581 insertions(+), 18 deletions(-) create mode 100644 src/Azure/Orleans.Persistence.AzureStorage.Migration/AzureBlobSiloBuilderExtensions.cs rename src/Azure/Orleans.Persistence.AzureStorage.Migration/{AzureBlobStorage.cs => MigrationAzureBlobGrainStorage.cs} (99%) create mode 100644 src/Orleans.Persistence.Migration/MigrationGrainStorage.cs create mode 100644 test/Extensions/Tester.AzureUtils.Migration/MigrationAzureBlobTests.cs create mode 100644 test/Extensions/Tester.AzureUtils.Migration/MigrationBaseTests.cs create mode 100644 test/Extensions/Tester.AzureUtils.Migration/Tester.AzureUtils.Migration.csproj diff --git a/Orleans.sln b/Orleans.sln index 083be67e50..37352b9e6d 100644 --- a/Orleans.sln +++ b/Orleans.sln @@ -222,6 +222,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Migration.Tests", "test\Ext EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Orleans.Persistence.AzureStorage.Migration", "src\Azure\Orleans.Persistence.AzureStorage.Migration\Orleans.Persistence.AzureStorage.Migration.csproj", "{0073C9D9-1906-452A-873E-9A2639FFB041}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tester.AzureUtils.Migration", "test\Extensions\Tester.AzureUtils.Migration\Tester.AzureUtils.Migration.csproj", "{C5529A77-1931-469B-AF8C-9CEEAE555FEA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1360,6 +1362,18 @@ Global {0073C9D9-1906-452A-873E-9A2639FFB041}.Release|x64.Build.0 = Release|Any CPU {0073C9D9-1906-452A-873E-9A2639FFB041}.Release|x86.ActiveCfg = Release|Any CPU {0073C9D9-1906-452A-873E-9A2639FFB041}.Release|x86.Build.0 = Release|Any CPU + {C5529A77-1931-469B-AF8C-9CEEAE555FEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5529A77-1931-469B-AF8C-9CEEAE555FEA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5529A77-1931-469B-AF8C-9CEEAE555FEA}.Debug|x64.ActiveCfg = Debug|Any CPU + {C5529A77-1931-469B-AF8C-9CEEAE555FEA}.Debug|x64.Build.0 = Debug|Any CPU + {C5529A77-1931-469B-AF8C-9CEEAE555FEA}.Debug|x86.ActiveCfg = Debug|Any CPU + {C5529A77-1931-469B-AF8C-9CEEAE555FEA}.Debug|x86.Build.0 = Debug|Any CPU + {C5529A77-1931-469B-AF8C-9CEEAE555FEA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5529A77-1931-469B-AF8C-9CEEAE555FEA}.Release|Any CPU.Build.0 = Release|Any CPU + {C5529A77-1931-469B-AF8C-9CEEAE555FEA}.Release|x64.ActiveCfg = Release|Any CPU + {C5529A77-1931-469B-AF8C-9CEEAE555FEA}.Release|x64.Build.0 = Release|Any CPU + {C5529A77-1931-469B-AF8C-9CEEAE555FEA}.Release|x86.ActiveCfg = Release|Any CPU + {C5529A77-1931-469B-AF8C-9CEEAE555FEA}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1472,6 +1486,7 @@ Global {E0B94181-200A-4CC6-94CA-D3D81CA6720A} = {FE2E08C6-9C3B-4AEE-AE07-CCA387580D7A} {57285DA6-6EF1-4B46-A178-1ABB48CD0B57} = {082D25DB-70CA-48F4-93E0-EC3455F494B8} {0073C9D9-1906-452A-873E-9A2639FFB041} = {4C5D66BF-EE1C-4DD8-8551-D1B7F3768A34} + {C5529A77-1931-469B-AF8C-9CEEAE555FEA} = {082D25DB-70CA-48F4-93E0-EC3455F494B8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7BFB3429-B5BB-4DB1-95B4-67D77A864952} diff --git a/src/Azure/Orleans.Persistence.AzureStorage.Migration/AzureBlobSiloBuilderExtensions.cs b/src/Azure/Orleans.Persistence.AzureStorage.Migration/AzureBlobSiloBuilderExtensions.cs new file mode 100644 index 0000000000..66158aa352 --- /dev/null +++ b/src/Azure/Orleans.Persistence.AzureStorage.Migration/AzureBlobSiloBuilderExtensions.cs @@ -0,0 +1,121 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Orleans.Configuration; +using Orleans.Hosting; +using Orleans.Providers; +using Orleans.Runtime; +using Orleans.Storage; +using Orleans.Storage.Migration.AzureStorage; + +namespace Orleans.Hosting +{ + public static class AzureBlobSiloBuilderExtensions + { + /// + /// Configure silo to use azure blob storage as the default grain storage. + /// + public static ISiloHostBuilder AddMigrationAzureBlobGrainStorageAsDefault(this ISiloHostBuilder builder, Action configureOptions) + { + return builder.AddMigrationAzureBlobGrainStorage(ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME, configureOptions); + } + + /// + /// Configure silo to use azure blob storage for grain storage. + /// + public static ISiloHostBuilder AddMigrationAzureBlobGrainStorage(this ISiloHostBuilder builder, string name, Action configureOptions) + { + return builder.ConfigureServices(services => services.AddMigrationAzureBlobGrainStorage(name, configureOptions)); + } + + /// + /// Configure silo to use azure blob storage as the default grain storage. + /// + public static ISiloHostBuilder AddMigrationAzureBlobGrainStorageAsDefault(this ISiloHostBuilder builder, Action> configureOptions = null) + { + return builder.AddMigrationAzureBlobGrainStorage(ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME, configureOptions); + } + + /// + /// Configure silo to use azure blob storage for grain storage. + /// + public static ISiloHostBuilder AddMigrationAzureBlobGrainStorage(this ISiloHostBuilder builder, string name, Action> configureOptions = null) + { + return builder.ConfigureServices(services => services.AddMigrationAzureBlobGrainStorage(name, configureOptions)); + } + + /// + /// Configure silo to use azure blob storage as the default grain storage. + /// + public static ISiloBuilder AddMigrationAzureBlobGrainStorageAsDefault(this ISiloBuilder builder, Action configureOptions) + { + return builder.AddMigrationAzureBlobGrainStorage(ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME, configureOptions); + } + + /// + /// Configure silo to use azure blob storage for grain storage. + /// + public static ISiloBuilder AddMigrationAzureBlobGrainStorage(this ISiloBuilder builder, string name, Action configureOptions) + { + return builder.ConfigureServices(services => services.AddMigrationAzureBlobGrainStorage(name, configureOptions)); + } + + /// + /// Configure silo to use azure blob storage as the default grain storage. + /// + public static ISiloBuilder AddMigrationAzureBlobGrainStorageAsDefault(this ISiloBuilder builder, Action> configureOptions = null) + { + return builder.AddMigrationAzureBlobGrainStorage(ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME, configureOptions); + } + + /// + /// Configure silo to use azure blob storage for grain storage. + /// + public static ISiloBuilder AddMigrationAzureBlobGrainStorage(this ISiloBuilder builder, string name, Action> configureOptions = null) + { + return builder.ConfigureServices(services => services.AddMigrationAzureBlobGrainStorage(name, configureOptions)); + } + + /// + /// Configure silo to use azure blob storage as the default grain storage. + /// + public static IServiceCollection AddMigrationAzureBlobGrainStorageAsDefault(this IServiceCollection services, Action configureOptions) + { + return services.AddMigrationAzureBlobGrainStorage(ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME, ob => ob.Configure(configureOptions)); + } + + /// + /// Configure silo to use azure blob storage for grain storage. + /// + public static IServiceCollection AddMigrationAzureBlobGrainStorage(this IServiceCollection services, string name, Action configureOptions) + { + return services.AddMigrationAzureBlobGrainStorage(name, ob => ob.Configure(configureOptions)); + } + + /// + /// Configure silo to use azure blob storage as the default grain storage. + /// + public static IServiceCollection AddMigrationAzureBlobGrainStorageAsDefault(this IServiceCollection services, Action> configureOptions = null) + { + return services.AddMigrationAzureBlobGrainStorage(ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME, configureOptions); + } + + /// + /// Configure silo to use azure blob storage for grain storage. + /// + public static IServiceCollection AddMigrationAzureBlobGrainStorage(this IServiceCollection services, string name, + Action> configureOptions = null) + { + configureOptions?.Invoke(services.AddOptions(name)); + services.AddTransient(sp => new AzureBlobStorageOptionsValidator(sp.GetRequiredService>().Get(name), name)); + services.ConfigureNamedOptionForLogging(name); + if (string.Equals(name, ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME, StringComparison.Ordinal)) + { + services.TryAddSingleton(sp => sp.GetServiceByName(ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME)); + } + return services.AddSingletonNamedService(name, MigrationAzureBlobGrainStorageFactory.Create) + .AddSingletonNamedService>(name, (s, n) => (ILifecycleParticipant)s.GetRequiredServiceByName(n)); + } + } +} diff --git a/src/Azure/Orleans.Persistence.AzureStorage.Migration/AzureBlobStorage.cs b/src/Azure/Orleans.Persistence.AzureStorage.Migration/MigrationAzureBlobGrainStorage.cs similarity index 99% rename from src/Azure/Orleans.Persistence.AzureStorage.Migration/AzureBlobStorage.cs rename to src/Azure/Orleans.Persistence.AzureStorage.Migration/MigrationAzureBlobGrainStorage.cs index ab953ec717..c44eb351c4 100644 --- a/src/Azure/Orleans.Persistence.AzureStorage.Migration/AzureBlobStorage.cs +++ b/src/Azure/Orleans.Persistence.AzureStorage.Migration/MigrationAzureBlobGrainStorage.cs @@ -351,7 +351,7 @@ private async Task Init(CancellationToken ct) public IAsyncEnumerable GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); } - public static class AzureBlobGrainStorageFactory + public static class MigrationAzureBlobGrainStorageFactory { public static IGrainStorage Create(IServiceProvider services, string name) { diff --git a/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureBlobStorage.cs b/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureBlobStorage.cs index 6b7e5bc5cc..7629ed94c0 100644 --- a/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureBlobStorage.cs +++ b/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureBlobStorage.cs @@ -314,7 +314,7 @@ private object ConvertFromStorageFormat(byte[] contents, Type stateType) public async IAsyncEnumerable GetAll([EnumeratorCancellation] CancellationToken cancellationToken) { - var regex = new Regex("(?\\w+)-(?\\w+).json"); + var regex = new Regex("(?[^-]+)-(?[^-]+).json"); await foreach (var item in this.container.GetBlobsAsync(cancellationToken: cancellationToken)) { var match = regex.Match(item.Name); diff --git a/src/Orleans.Persistence.Migration/GrainReferenceExtractor.cs b/src/Orleans.Persistence.Migration/GrainReferenceExtractor.cs index 573a753177..57eda47ac1 100644 --- a/src/Orleans.Persistence.Migration/GrainReferenceExtractor.cs +++ b/src/Orleans.Persistence.Migration/GrainReferenceExtractor.cs @@ -55,32 +55,41 @@ public GrainReferenceExtractor( Type grainType = LookupType(grainClass) ?? throw new ArgumentException("Grain type not found"); var type = _grainTypeResolver.GetGrainType(grainType); - // Get GrainInterfaceType - Type iface = null; - if (_grainTypeManager.GrainTypeResolver.TryGetInterfaceData(grainReference.InterfaceId, out var interfaceData)) + GrainInterfaceType interfaceType; + try { - if (interfaceData.Interface.IsGenericType) + // Get GrainInterfaceType + Type iface = null; + if (_grainTypeManager.GrainTypeResolver.TryGetInterfaceData(grainReference.InterfaceId, out var interfaceData)) { - // We cannot use grainReference.InterfaceName because it doesn't match - foreach (var candidate in grainType.GetInterfaces()) + if (interfaceData.Interface.IsGenericType) { - if (candidate.Name.Equals(interfaceData.Interface.Name, StringComparison.OrdinalIgnoreCase)) + // We cannot use grainReference.InterfaceName because it doesn't match + foreach (var candidate in grainType.GetInterfaces()) { - iface = candidate; - break; + if (candidate.Name.Equals(interfaceData.Interface.Name, StringComparison.OrdinalIgnoreCase)) + { + iface = candidate; + break; + } } } + else + { + iface = interfaceData.Interface; + } } - else + if (iface == null) { - iface = interfaceData.Interface; + throw new ArgumentException("Grain interface type not found"); } + interfaceType = _grainInterfaceTypeResolver.GetGrainInterfaceType(iface); } - if (iface == null) + catch (InvalidOperationException) { - throw new ArgumentException("Grain interface type not found"); + // The GrainReference doesn't include the interface. It's fine, ignore it and let interfaceType to be blank + interfaceType = default; } - var interfaceType = _grainInterfaceTypeResolver.GetGrainInterfaceType(iface); // Extract Key IdSpan key; diff --git a/src/Orleans.Persistence.Migration/HostingExtensions.cs b/src/Orleans.Persistence.Migration/HostingExtensions.cs index 40780353a9..99d1b65900 100644 --- a/src/Orleans.Persistence.Migration/HostingExtensions.cs +++ b/src/Orleans.Persistence.Migration/HostingExtensions.cs @@ -1,9 +1,14 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using Orleans.Hosting; using Orleans.Metadata; using Orleans.Persistence.Migration.Serialization; +using Orleans.Providers; +using Orleans.Runtime; using Orleans.Serialization.TypeSystem; +using Orleans.Storage; +using CachedTypeResolver = Orleans.Serialization.TypeSystem.CachedTypeResolver; namespace Orleans.Persistence.Migration { @@ -16,14 +21,85 @@ public static ISiloBuilder AddMigrationTools(this ISiloBuilder builder) services .AddSingleton, ConfigureOrleansJsonSerializerOptions>() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton(); }); return builder; } + + /// + /// Configure silo to use migration storage as the default grain storage. + /// + public static ISiloBuilder AddMigrationGrainStorageAsDefault(this ISiloBuilder builder, Action configureOptions) + { + return builder.AddMigrationGrainStorage(ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME, configureOptions); + } + + /// + /// Configure silo to use migration storage for grain storage. + /// + public static ISiloBuilder AddMigrationGrainStorage(this ISiloBuilder builder, string name, Action configureOptions) + { + return builder.ConfigureServices(services => services.AddMigrationGrainStorage(name, configureOptions)); + } + + /// + /// Configure silo to use migration storage as the default grain storage. + /// + public static ISiloBuilder AddMigrationGrainStorageAsDefault(this ISiloBuilder builder, Action> configureOptions = null) + { + return builder.AddMigrationGrainStorage(ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME, configureOptions); + } + + /// + /// Configure silo to use migration storage for grain storage. + /// + public static ISiloBuilder AddMigrationGrainStorage(this ISiloBuilder builder, string name, Action> configureOptions = null) + { + return builder.ConfigureServices(services => services.AddMigrationGrainStorage(name, configureOptions)); + } + + /// + /// Configure silo to use migration storage as the default grain storage. + /// + public static IServiceCollection AddMigrationGrainStorageAsDefault(this IServiceCollection services, Action configureOptions) + { + return services.AddMigrationGrainStorage(ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME, ob => ob.Configure(configureOptions)); + } + + /// + /// Configure silo to use migration storage for grain storage. + /// + public static IServiceCollection AddMigrationGrainStorage(this IServiceCollection services, string name, Action configureOptions) + { + return services.AddMigrationGrainStorage(name, ob => ob.Configure(configureOptions)); + } + + /// + /// Configure silo to use migration storage as the default grain storage. + /// + public static IServiceCollection AddMigrationGrainStorage(this IServiceCollection services, Action> configureOptions = null) + { + return services.AddMigrationGrainStorage(ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME, configureOptions); + } + + /// + /// Configure silo to use migration storage for grain storage. + /// + public static IServiceCollection AddMigrationGrainStorage(this IServiceCollection services, string name, Action> configureOptions) + { + configureOptions?.Invoke(services.AddOptions(name)); + if (string.Equals(name, ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME, StringComparison.Ordinal)) + { + services.TryAddSingleton(sp => sp.GetServiceByName(ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME)); + } + services.AddSingletonNamedService(name, MigrationGrainStorage.Create); + return services; + } } } diff --git a/src/Orleans.Persistence.Migration/MigrationGrainStorage.cs b/src/Orleans.Persistence.Migration/MigrationGrainStorage.cs new file mode 100644 index 0000000000..10e8747c08 --- /dev/null +++ b/src/Orleans.Persistence.Migration/MigrationGrainStorage.cs @@ -0,0 +1,104 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Orleans.Runtime; +using Orleans.Storage; + +namespace Orleans.Persistence.Migration +{ + public class MigrationGrainStorage : IGrainStorage + { + [Serializable] + private struct MigrationEtag + { + public string SourceETag { get; set; } + public string DestinationETag { get; set; } + + public MigrationEtag(string sourceETag, string destinationETag) + { + this.SourceETag = sourceETag; + this.DestinationETag = destinationETag; + } + + public string SerializeToJson() => JsonConvert.SerializeObject(this, typeof(MigrationEtag), null); + + public static string SerializeToJson(string sourceETag, string destinationETag) + { + var obj = new MigrationEtag(sourceETag, destinationETag); + return JsonConvert.SerializeObject(obj); + } + + public static MigrationEtag ParseFromJson(string json) => (MigrationEtag) JsonConvert.DeserializeObject(json, typeof(MigrationEtag)); + } + + private readonly IGrainStorage _sourceStorage; + private readonly IGrainStorage _destinationStorage; + + public MigrationGrainStorage(IGrainStorage sourceStorage, IGrainStorage destinationStorage) + { + this._sourceStorage = sourceStorage; + this._destinationStorage = destinationStorage; + } + + public async Task ClearStateAsync(string grainType, GrainReference grainReference, IGrainState grainState) + { + var eTag = MigrationEtag.ParseFromJson(grainState.ETag); + grainState.ETag = eTag.SourceETag; + await _sourceStorage.ClearStateAsync(grainType, grainReference, grainState); + grainState.ETag = eTag.DestinationETag; + await _destinationStorage.ClearStateAsync(grainType, grainReference, grainState); + } + + public async Task ReadStateAsync(string grainType, GrainReference grainReference, IGrainState grainState) + { + await _destinationStorage.ReadStateAsync(grainType, grainReference, grainState); + if (grainState.RecordExists) + { + grainState.ETag = MigrationEtag.SerializeToJson(null, grainState.ETag); + } + else + { + await _sourceStorage.ReadStateAsync(grainType, grainReference, grainState); + grainState.ETag = MigrationEtag.SerializeToJson(grainState.ETag, null); + } + } + + public async Task WriteStateAsync(string grainType, GrainReference grainReference, IGrainState grainState) + { + MigrationEtag etag = new MigrationEtag(); + if (grainState.RecordExists) + { + var eTag = MigrationEtag.ParseFromJson(grainState.ETag); + grainState.ETag = eTag.DestinationETag; + } + try + { + await _destinationStorage.WriteStateAsync(grainType, grainReference, grainState); + etag.DestinationETag = grainState.ETag; + } + finally + { + grainState.ETag = etag.SerializeToJson(); + } + } + + public IAsyncEnumerable GetAll(CancellationToken cancellationToken) => throw new NotImplementedException(); + + public static IGrainStorage Create(IServiceProvider serviceProvider, string name) + { + var options = serviceProvider + .GetRequiredService>() + .Get(name); + var source = serviceProvider.GetRequiredServiceByName(options.SourceStorageName); + var destination = serviceProvider.GetRequiredServiceByName(options.DestinationStorageName); + return new MigrationGrainStorage(source, destination); + } + } + + public class MigrationGrainStorageOptions + { + public string SourceStorageName { get; set; } + + public string DestinationStorageName { get; set; } + } +} diff --git a/test/Extensions/Tester.AzureUtils.Migration/MigrationAzureBlobTests.cs b/test/Extensions/Tester.AzureUtils.Migration/MigrationAzureBlobTests.cs new file mode 100644 index 0000000000..39a096e908 --- /dev/null +++ b/test/Extensions/Tester.AzureUtils.Migration/MigrationAzureBlobTests.cs @@ -0,0 +1,49 @@ +using Orleans.Hosting; +using Orleans.Persistence.Migration; +using Orleans.TestingHost; +using Xunit; + +namespace Tester.AzureUtils.Migration +{ + [TestCategory("Functionals"), TestCategory("Migration"), TestCategory("Azure"), TestCategory("AzureBlobStorage")] + public class MigrationAzureBlobTests : MigrationBaseTests, IClassFixture + { + public static Guid Guid = Guid.NewGuid(); + + public class Fixture : BaseAzureTestClusterFixture + { + protected override void ConfigureTestCluster(TestClusterBuilder builder) + { + builder.AddSiloBuilderConfigurator(); + } + } + + private class SiloConfigurator : ISiloConfigurator + { + public void Configure(ISiloBuilder siloBuilder) + { + siloBuilder + .AddMigrationTools() + .AddMigrationGrainStorageAsDefault(options => + { + options.SourceStorageName = SourceStorageName; + options.DestinationStorageName = DestinationStorageName; + }) + .AddAzureBlobGrainStorage(SourceStorageName, options => + { + options.ConfigureTestDefaults(); + options.ContainerName = $"source{Guid}"; + }) + .AddMigrationAzureBlobGrainStorage(DestinationStorageName, options => + { + options.ConfigureTestDefaults(); + options.ContainerName = $"destination{Guid}"; + }); + } + } + + public MigrationAzureBlobTests(Fixture fixture) : base(fixture) + { + } + } +} diff --git a/test/Extensions/Tester.AzureUtils.Migration/MigrationBaseTests.cs b/test/Extensions/Tester.AzureUtils.Migration/MigrationBaseTests.cs new file mode 100644 index 0000000000..9c55cdc7dc --- /dev/null +++ b/test/Extensions/Tester.AzureUtils.Migration/MigrationBaseTests.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Orleans; +using Orleans.Providers; +using Orleans.Runtime; +using Orleans.Storage; +using Orleans.TestingHost; +using TestExtensions; +using UnitTests.GrainInterfaces; +using UnitTests.Grains; +using Xunit; + +namespace Tester.AzureUtils.Migration +{ + public abstract class MigrationBaseTests + { + protected BaseAzureTestClusterFixture fixture; + public const string SourceStorageName = "source-storage"; + public const string DestinationStorageName = "destination-storage"; + + private IServiceProvider? serviceProvider; + private IServiceProvider ServiceProvider + { + get + { + if (this.serviceProvider == null) + { + var silo = (InProcessSiloHandle)this.fixture.HostedCluster.Primary; + this.serviceProvider = silo.SiloHost.Services; + } + return this.serviceProvider; + } + } + + private IGrainStorage? sourceStorage; + private IGrainStorage SourceStorage + { + get + { + if (this.sourceStorage == null) + { + this.sourceStorage = ServiceProvider.GetRequiredServiceByName(SourceStorageName); + } + return this.sourceStorage; + } + } + + private IGrainStorage? destinationStorage; + private IGrainStorage DestinationStorage + { + get + { + if (this.destinationStorage == null) + { + this.destinationStorage = ServiceProvider.GetRequiredServiceByName(DestinationStorageName); + } + return this.destinationStorage; + } + } + + private IGrainStorage? migrationStorage; + private IGrainStorage MigrationStorage + { + get + { + if (this.migrationStorage == null) + { + this.migrationStorage = ServiceProvider.GetRequiredServiceByName(ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME); + } + return this.migrationStorage; + } + } + + protected MigrationBaseTests(BaseAzureTestClusterFixture fixture) + { + this.fixture = fixture; + } + + [Fact] + public async Task ReadFromSourceTest() + { + var grain = this.fixture.Client.GetGrain(100); + var grainState = new GrainState(new() { A = 33, B = 806 }); + var stateName = (typeof(SimplePersistentGrain)).FullName; + + // Write directly to source storage + await SourceStorage.WriteStateAsync(stateName, (GrainReference) grain, grainState); + + Assert.Equal(grainState.State.A, await grain.GetA()); + Assert.Equal(grainState.State.A * grainState.State.B, await grain.GetAxB()); + } + + [Fact] + public async Task ReadFromTargetTest() + { + var grain = this.fixture.Client.GetGrain(200); + var oldGrainState = new GrainState(new() { A = 33, B = 806 }); + var newGrainState = new GrainState(new() { A = 20, B = 30 }); + var stateName = (typeof(SimplePersistentGrain)).FullName; + + // Write directly to storages + await SourceStorage.WriteStateAsync(stateName, (GrainReference)grain, oldGrainState); + await DestinationStorage.WriteStateAsync(stateName, (GrainReference)grain, newGrainState); + + Assert.Equal(newGrainState.State.A, await grain.GetA()); + Assert.Equal(newGrainState.State.A * newGrainState.State.B, await grain.GetAxB()); + } + + [Fact] + public async Task ReadFromSourceThenWriteToTargetTest() + { + var grain = this.fixture.Client.GetGrain(300); + var oldGrainState = new GrainState(new() { A = 33, B = 806 }); + var newState = new SimplePersistentGrain_State { A = 20, B = 30 }; + var stateName = (typeof(SimplePersistentGrain)).FullName; + + // Write directly to source storage + await SourceStorage.WriteStateAsync(stateName, (GrainReference)grain, oldGrainState); + + // Grain should read from source but write to destination + Assert.Equal(oldGrainState.State.A, await grain.GetA()); + Assert.Equal(oldGrainState.State.A * oldGrainState.State.B, await grain.GetAxB()); + await grain.SetA(newState.A); + await grain.SetB(newState.B); + + var newGrainState = new GrainState(); + await DestinationStorage.ReadStateAsync(stateName, (GrainReference)grain, newGrainState); + + Assert.Equal(newGrainState.State.A, await grain.GetA()); + Assert.Equal(newGrainState.State.A * newGrainState.State.B, await grain.GetAxB()); + } + + [Fact] + public async Task ClearAllTest() + { + var grain = this.fixture.Client.GetGrain(400); + var oldGrainState = new GrainState(new() { A = 33, B = 806 }); + var newGrainState = new GrainState(new() { A = 20, B = 30 }); + var stateName = (typeof(SimplePersistentGrain)).FullName; + + // Write directly to storages + await SourceStorage.WriteStateAsync(stateName, (GrainReference)grain, oldGrainState); + await DestinationStorage.WriteStateAsync(stateName, (GrainReference)grain, newGrainState); + + // Clear + var migratedState = new GrainState(); + await MigrationStorage.ReadStateAsync(stateName, (GrainReference)grain, migratedState); + await MigrationStorage.ClearStateAsync(stateName, (GrainReference)grain, migratedState); + + // Read + var oldGrainState2 = new GrainState(); + var newGrainState2 = new GrainState(); + await SourceStorage.ReadStateAsync(stateName, (GrainReference)grain, oldGrainState2); + await DestinationStorage.ReadStateAsync(stateName, (GrainReference)grain, newGrainState2); + Assert.False(oldGrainState2.RecordExists); + Assert.False(newGrainState2.RecordExists); + } + } +} diff --git a/test/Extensions/Tester.AzureUtils.Migration/Tester.AzureUtils.Migration.csproj b/test/Extensions/Tester.AzureUtils.Migration/Tester.AzureUtils.Migration.csproj new file mode 100644 index 0000000000..dcef2668d5 --- /dev/null +++ b/test/Extensions/Tester.AzureUtils.Migration/Tester.AzureUtils.Migration.csproj @@ -0,0 +1,26 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + + + + + + + + diff --git a/test/TesterInternal/StorageTests/TestDataSets/TestStateGeneric1.cs b/test/TesterInternal/StorageTests/TestDataSets/TestStateGeneric1.cs index de29bea096..c3c25b6ee7 100644 --- a/test/TesterInternal/StorageTests/TestDataSets/TestStateGeneric1.cs +++ b/test/TesterInternal/StorageTests/TestDataSets/TestStateGeneric1.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; From cf9cd2e66f17349c25be61a4b3511ce50de70c19 Mon Sep 17 00:00:00 2001 From: Benjamin Petit Date: Thu, 19 Jan 2023 14:52:27 +0100 Subject: [PATCH 5/5] Hack to make build pass --- .../MigrationAzureBlobTests.cs | 4 +++- .../Tester.AzureUtils.Migration.csproj | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/test/Extensions/Tester.AzureUtils.Migration/MigrationAzureBlobTests.cs b/test/Extensions/Tester.AzureUtils.Migration/MigrationAzureBlobTests.cs index 39a096e908..be2ed0229a 100644 --- a/test/Extensions/Tester.AzureUtils.Migration/MigrationAzureBlobTests.cs +++ b/test/Extensions/Tester.AzureUtils.Migration/MigrationAzureBlobTests.cs @@ -1,4 +1,5 @@ -using Orleans.Hosting; +#if NET70 +using Orleans.Hosting; using Orleans.Persistence.Migration; using Orleans.TestingHost; using Xunit; @@ -47,3 +48,4 @@ public MigrationAzureBlobTests(Fixture fixture) : base(fixture) } } } +#endif diff --git a/test/Extensions/Tester.AzureUtils.Migration/Tester.AzureUtils.Migration.csproj b/test/Extensions/Tester.AzureUtils.Migration/Tester.AzureUtils.Migration.csproj index dcef2668d5..0c2a5b70b9 100644 --- a/test/Extensions/Tester.AzureUtils.Migration/Tester.AzureUtils.Migration.csproj +++ b/test/Extensions/Tester.AzureUtils.Migration/Tester.AzureUtils.Migration.csproj @@ -1,11 +1,15 @@ - net7.0 + $(TestTargetFrameworks);net7.0 enable enable + + NET70 + + @@ -15,7 +19,7 @@ - +