diff --git a/src/Orleans.sln b/src/Orleans.sln index 836cdb9655a..25a07ee7b1f 100644 --- a/src/Orleans.sln +++ b/src/Orleans.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 +VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{013DFD29-E1DB-4968-A67B-C2342E6F5B6E}" ProjectSection(SolutionItems) = preProject diff --git a/src/OrleansAzureUtils/Providers/Storage/AzureTableStorage.cs b/src/OrleansAzureUtils/Providers/Storage/AzureTableStorage.cs index c5a5d83f2b4..438fe6ac714 100644 --- a/src/OrleansAzureUtils/Providers/Storage/AzureTableStorage.cs +++ b/src/OrleansAzureUtils/Providers/Storage/AzureTableStorage.cs @@ -139,11 +139,11 @@ public async Task ReadStateAsync(string grainType, GrainReference grainReference { if (tableDataManager == null) throw new ArgumentException("GrainState-Table property not initialized"); - string pk = GetKeyString(grainReference); + string pk = GetPartitionKey(grainReference); if (Log.IsVerbose3) Log.Verbose3((int)AzureProviderErrorCode.AzureTableProvider_ReadingData, "Reading: GrainType={0} Pk={1} Grainid={2} from Table={3}", grainType, pk, grainReference, tableName); - string partitionKey = pk; - string rowKey = grainType; - GrainStateRecord record = await tableDataManager.Read(partitionKey, rowKey).ConfigureAwait(false); + + string rk = GetRowKey(grainType); + GrainStateRecord record = await tableDataManager.Read(pk, rk).ConfigureAwait(false); if (record != null) { var entity = record.Entity; @@ -164,11 +164,13 @@ public async Task WriteStateAsync(string grainType, GrainReference grainReferenc { if (tableDataManager == null) throw new ArgumentException("GrainState-Table property not initialized"); - string pk = GetKeyString(grainReference); + string pk = GetPartitionKey(grainReference); if (Log.IsVerbose3) Log.Verbose3((int)AzureProviderErrorCode.AzureTableProvider_WritingData, "Writing: GrainType={0} Pk={1} Grainid={2} ETag={3} to Table={4}", grainType, pk, grainReference, grainState.ETag, tableName); - var entity = new DynamicTableEntity(pk, grainType); + string rk = GetRowKey(grainType); + var entity = new DynamicTableEntity(pk, rk); + ConvertToStorageFormat(grainState.State, entity); var record = new GrainStateRecord { Entity = entity, ETag = grainState.ETag }; try @@ -195,9 +197,12 @@ public async Task ClearStateAsync(string grainType, GrainReference grainReferenc { if (tableDataManager == null) throw new ArgumentException("GrainState-Table property not initialized"); - string pk = GetKeyString(grainReference); + string pk = GetPartitionKey(grainReference); if (Log.IsVerbose3) Log.Verbose3((int)AzureProviderErrorCode.AzureTableProvider_WritingData, "Clearing: GrainType={0} Pk={1} Grainid={2} ETag={3} DeleteStateOnClear={4} from Table={5}", grainType, pk, grainReference, grainState.ETag, isDeleteStateOnClear, tableName); - var entity = new DynamicTableEntity(pk, grainType); + + string rk = GetRowKey(grainType); + var entity = new DynamicTableEntity(pk, rk); + var record = new GrainStateRecord { Entity = entity, ETag = grainState.ETag }; string operation = "Clearing"; try @@ -436,12 +441,27 @@ internal object ConvertFromStorageFormat(DynamicTableEntity entity) return dataValue; } - private string GetKeyString(GrainReference grainReference) + private string GetPartitionKey(GrainReference grainReference) { var key = String.Format("{0}_{1}", serviceId, grainReference.ToKeyString()); return AzureStorageUtils.SanitizeTableProperty(key); } + + private string GetRowKey(string grainType) + { + string rowKey; + int index = grainType.IndexOf("`"); + if (index > 0) + // in case we have a generic grain, we use an empty rowkey as it seems Azure has a maximum length for the combined keys? + return String.Empty; + else + // for non-generic grains, we keep the existing behavior to not break existing storage, but we do sanitize + rowKey = AzureStorageUtils.SanitizeTableProperty(grainType); + + return rowKey; + } + internal class GrainStateRecord { public string ETag { get; set; } diff --git a/src/OrleansAzureUtils/Storage/AzureStorageUtils.cs b/src/OrleansAzureUtils/Storage/AzureStorageUtils.cs index fbd14431f28..5ef7d3b656a 100644 --- a/src/OrleansAzureUtils/Storage/AzureStorageUtils.cs +++ b/src/OrleansAzureUtils/Storage/AzureStorageUtils.cs @@ -6,6 +6,7 @@ using Microsoft.WindowsAzure.Storage.RetryPolicies; using Microsoft.WindowsAzure.Storage.Shared.Protocol; using Orleans.Runtime; +using System.Text; namespace Orleans.AzureUtils { @@ -274,16 +275,24 @@ public static string SanitizeTableProperty(string key) { // Remove any characters that can't be used in Azure PartitionKey or RowKey values // http://www.jamestharpe.com/web-development/azure-table-service-character-combinations-disallowed-in-partitionkey-rowkey/ - key = key - .Replace('/', '_') // Forward slash - .Replace('\\', '_') // Backslash - .Replace('#', '_') // Pound sign - .Replace('?', '_'); // Question mark + var sb = new StringBuilder(); + foreach (var c in key) + { + if (c == '/' + || c == '\\' + || c == '#' + || c == '/' + || c == '?' + || char.IsControl(c)) + sb.Append('_'); + else + sb.Append(c); + } - if (key.Length >= 1024) + if (sb.Length >= 1024) throw new ArgumentException(string.Format("Key length {0} is too long to be an Azure table key. Key={1}", key.Length, key)); - return key; + return sb.ToString(); } diff --git a/test/Tester/GenericGrainsInAzureStorageTests.cs b/test/Tester/GenericGrainsInAzureStorageTests.cs index dd17a5b1899..3b4ee2254a0 100644 --- a/test/Tester/GenericGrainsInAzureStorageTests.cs +++ b/test/Tester/GenericGrainsInAzureStorageTests.cs @@ -30,7 +30,7 @@ public async Task Generic_OnAzureTableStorage_LongNamedGrain_EchoValue() await grain.EchoAsync(42); //ClearState() also exhibits the error, even with the shorter named grain - //await grain.ClearState(); + await grain.ClearState(); } [Fact, TestCategory("Azure"), TestCategory("Functional"), TestCategory("Generics")]