diff --git a/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureTableStorage.cs b/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureTableStorage.cs index 19b5d57856..03fcdf2b8e 100644 --- a/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureTableStorage.cs +++ b/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureTableStorage.cs @@ -40,6 +40,7 @@ public class AzureTableGrainStorage : IGrainStorage, IRestExceptionDecoder, ILif private const string BINARY_DATA_PROPERTY_NAME = "Data"; private const string STRING_DATA_PROPERTY_NAME = "StringData"; + private readonly string name; /// Default constructor @@ -202,23 +203,29 @@ private static async Task DoOptimisticUpdate(Func updateOperation, string /// internal void ConvertToStorageFormat(T grainState, TableEntity entity) { - int dataSize; - IEnumerable> properties; - string basePropertyName; + var binaryData = storageSerializer.Serialize(grainState); - // Convert to binary format - var data = this.storageSerializer.Serialize(grainState); - basePropertyName = BINARY_DATA_PROPERTY_NAME; + CheckMaxDataSize(binaryData.ToMemory().Length, MAX_DATA_CHUNK_SIZE * MAX_DATA_CHUNKS_COUNT); - dataSize = data.ToMemory().Length; - properties = SplitBinaryData(data); + if (options.UseStringFormat) + { + var properties = SplitStringData(binaryData.ToString().AsMemory()); - CheckMaxDataSize(dataSize, MAX_DATA_CHUNK_SIZE * MAX_DATA_CHUNKS_COUNT); + foreach (var keyValuePair in properties.Zip(GetPropertyNames(STRING_DATA_PROPERTY_NAME), + (property, name) => new KeyValuePair(name, property.ToString()))) + { + entity[keyValuePair.Key] = keyValuePair.Value; + } + } + else + { + var properties = SplitBinaryData(binaryData); - foreach (var keyValuePair in properties.Zip(GetPropertyNames(basePropertyName), + foreach (var keyValuePair in properties.Zip(GetPropertyNames(BINARY_DATA_PROPERTY_NAME), (property, name) => new KeyValuePair(name, property.ToArray()))) - { - entity[keyValuePair.Key] = keyValuePair.Value; + { + entity[keyValuePair.Key] = keyValuePair.Value; + } } } diff --git a/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureTableStorageOptions.cs b/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureTableStorageOptions.cs index a854848ecd..ba5f7a800e 100644 --- a/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureTableStorageOptions.cs +++ b/src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureTableStorageOptions.cs @@ -19,6 +19,11 @@ public class AzureTableStorageOptions : AzureStorageOperationOptions, IStoragePr /// public bool DeleteStateOnClear { get; set; } = false; + /// + /// Indicates if grain data should be stored in string or in binary format. + /// + public bool UseStringFormat { get; set; } + /// /// Stage of silo lifecycle where storage should be initialized. Storage must be initialized prior to use. /// diff --git a/test/Extensions/TesterAzureUtils/Persistence/PersistenceProviderTests.cs b/test/Extensions/TesterAzureUtils/Persistence/PersistenceProviderTests.cs index d33c5bff2e..5fdb8e1105 100644 --- a/test/Extensions/TesterAzureUtils/Persistence/PersistenceProviderTests.cs +++ b/test/Extensions/TesterAzureUtils/Persistence/PersistenceProviderTests.cs @@ -166,6 +166,40 @@ public async Task PersistenceProvider_Azure_ChangeWriteFormat(int? stringLength, await Test_PersistenceProvider_WriteRead(testName, store, grainState, grainId); } + [SkippableTheory, TestCategory("Functional"), TestCategory("AzureStorage")] + [InlineData(null, true, false)] + [InlineData(null, false, true)] + [InlineData(15 * 32 * 1024 - 256, true, false)] + [InlineData(15 * 32 * 1024 - 256, false, true)] + public async Task PersistenceProvider_Azure_ChangeStorageDataFormat_WhenJsonSerializerIsUsed(int? stringLength, bool useStringFormatForFirstWrite, bool useStringFormatForSecondWrite) + { + // always use JsonSerializer over OrleansSerializer since specifying 'useStringFormat = true' + // writes to 'StringData', the OrleansSerializer can not read from the 'StringData' column as its not a format which it expects. + const bool useJson = true; + + var testName = string.Format("{0}({1}={2},{3}={4},{5}={6})", + nameof(PersistenceProvider_Azure_ChangeStorageDataFormat_WhenJsonSerializerIsUsed), + nameof(stringLength), stringLength == null ? "default" : stringLength.ToString(), + "strFormat1stW", useStringFormatForFirstWrite, + "strFormat2ndW", useStringFormatForSecondWrite); + + var grainState = TestStoreGrainState.NewRandomState(stringLength); + EnsureEnvironmentSupportsState(grainState); + + var grainId = LegacyGrainId.NewId(); + + var store = await InitAzureTableGrainStorage(useJson: useJson, useStringFormat: useStringFormatForFirstWrite); + + await Test_PersistenceProvider_WriteRead(testName, store, grainState, grainId); + + grainState = TestStoreGrainState.NewRandomState(stringLength); + grainState.ETag = "*"; + + store = await InitAzureTableGrainStorage(useJson: useJson, useStringFormat: useStringFormatForSecondWrite); + + await Test_PersistenceProvider_WriteRead(testName, store, grainState, grainId); + } + [SkippableTheory, TestCategory("Functional"), TestCategory("AzureStorage")] [InlineData(null, false)] [InlineData(null, true)] @@ -266,8 +300,13 @@ private async Task InitAzureTableGrainStorage(AzureTable return store; } - private async Task InitAzureTableGrainStorage(bool useJson = false, TypeNameHandling? typeNameHandling = null) + private async Task InitAzureTableGrainStorage(bool useJson = false, bool useStringFormat = false, TypeNameHandling? typeNameHandling = null) { + if (useStringFormat && !useJson) + { + throw new InvalidOperationException($"Using {nameof(OrleansGrainStorageSerializer)} in conjuction with string data format makes no sense, there for stopping attempt."); + } + var options = new AzureTableStorageOptions(); var jsonOptions = this.providerRuntime.ServiceProvider.GetService>(); if (typeNameHandling != null) @@ -276,6 +315,7 @@ private async Task InitAzureTableGrainStorage(bool useJs } options.ConfigureTestDefaults(); + options.UseStringFormat = useStringFormat; // TODO change test to include more serializer? var binarySerializer = new OrleansGrainStorageSerializer(this.providerRuntime.ServiceProvider.GetRequiredService());