From 77cb07999516b9cae58b28d06d3f9a3eef83577a Mon Sep 17 00:00:00 2001 From: Vladislav Prishchepa Date: Tue, 12 Nov 2024 22:17:10 +0200 Subject: [PATCH] ADO.NET `IHashPicker` customization API + Orleans v3-compatible `IHashPicker` implementation (#9217) * Allow to customize AdoNetGrainStorage.HashPicker via AdoNetGrainStorageOptions * Orleans v3-compatible IHasher implementation added * custom Orleans v3-compatible IHashPicker implementation added: string-only grain id serialization breaking changes handling required * UseOrleans3CompatibleHasher() method fix * Orleans3CompatibleHasher byte[] allocations eliminated, JenkinsHash unused methods removed * JenkinsHash optimization, Orleans3CompatibleStringKeyHasher refactoring * Orleans3CompatibleStringKeyHasher refactoring * Orleans3CompatibleStringKeyHasher refactoring * default IHashPicker change reverted * IHashPicker configuration comments fix * AdoNetGrainStorage.HashPicker assignment fallback in ctor --- .../Options/AdoNetGrainStorageOptions.cs | 37 +++++++ .../Storage/Provider/AdoNetGrainStorage.cs | 3 +- ...GrainStorageServiceCollectionExtensions.cs | 1 + .../Storage/Provider/JenkinsHash.cs | 85 ++++++++++++++++ .../Provider/Orleans3CompatibleHasher.cs | 29 ++++++ .../Orleans3CompatibleStorageHashPicker.cs | 46 +++++++++ .../Orleans3CompatibleStringKeyHasher.cs | 98 +++++++++++++++++++ 7 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/JenkinsHash.cs create mode 100644 src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/Orleans3CompatibleHasher.cs create mode 100644 src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/Orleans3CompatibleStorageHashPicker.cs create mode 100644 src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/Orleans3CompatibleStringKeyHasher.cs diff --git a/src/AdoNet/Orleans.Persistence.AdoNet/Options/AdoNetGrainStorageOptions.cs b/src/AdoNet/Orleans.Persistence.AdoNet/Options/AdoNetGrainStorageOptions.cs index ff50d59021..09bb207b51 100644 --- a/src/AdoNet/Orleans.Persistence.AdoNet/Options/AdoNetGrainStorageOptions.cs +++ b/src/AdoNet/Orleans.Persistence.AdoNet/Options/AdoNetGrainStorageOptions.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.Options; using Orleans.Persistence.AdoNet.Storage; using Orleans.Runtime; using Orleans.Storage; @@ -37,6 +38,21 @@ public class AdoNetGrainStorageOptions : IStorageProviderSerializerOptions /// public IGrainStorageSerializer GrainStorageSerializer { get; set; } + + /// + /// Gets or sets the hasher picker to use for this storage provider. + /// + public IStorageHasherPicker HashPicker { get; set; } + + /// + /// Sets legacy Orleans v3-compatible hash picker to use for this storage provider. Invoke this method if you need to run + /// Orleans v7+ silo against existing Orleans v3-initialized database and keep existing grain state. + /// + public void UseOrleans3CompatibleHasher() + { + // content-aware hashing with different pickers, unable to use standard StorageHasherPicker + this.HashPicker = new Orleans3CompatibleStorageHashPicker(); + } } /// @@ -70,6 +86,27 @@ public void ValidateConfiguration() { throw new OrleansConfigurationException($"Invalid {nameof(AdoNetGrainStorageOptions)} values for {nameof(AdoNetGrainStorage)} \"{name}\". {nameof(options.ConnectionString)} is required."); } + + if (this.options.HashPicker == null) + { + throw new OrleansConfigurationException($"Invalid {nameof(AdoNetGrainStorageOptions)} values for {nameof(AdoNetGrainStorage)} {name}. {nameof(options.HashPicker)} is required."); + } + } + } + + /// + /// Provides default configuration HashPicker for AdoNetGrainStorageOptions. + /// + public class DefaultAdoNetGrainStorageOptionsHashPickerConfigurator : IPostConfigureOptions + { + public void PostConfigure(string name, AdoNetGrainStorageOptions options) + { + // preserving explicitly configured HashPicker + if (options.HashPicker != null) + return; + + // set default IHashPicker if not configured yet + options.HashPicker = new StorageHasherPicker(new[] { new OrleansDefaultHasher() }); } } } diff --git a/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/AdoNetGrainStorage.cs b/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/AdoNetGrainStorage.cs index cad1a83435..f10bc65e8e 100644 --- a/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/AdoNetGrainStorage.cs +++ b/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/AdoNetGrainStorage.cs @@ -118,7 +118,7 @@ public class AdoNetGrainStorage: IGrainStorage, ILifecycleParticipant /// The hash generator used to hash natural keys, grain ID and grain type to a more narrow index. /// - public IStorageHasherPicker HashPicker { get; set; } = new StorageHasherPicker(new[] { new OrleansDefaultHasher() }); + public IStorageHasherPicker HashPicker { get; set; } private readonly AdoNetGrainStorageOptions options; private readonly IProviderRuntime providerRuntime; @@ -137,6 +137,7 @@ public AdoNetGrainStorage( this.logger = logger; this.serviceId = clusterOptions.Value.ServiceId; this.Serializer = options.Value.GrainStorageSerializer; + this.HashPicker = options.Value.HashPicker ?? new StorageHasherPicker(new[] { new OrleansDefaultHasher() });; } public void Participate(ISiloLifecycle lifecycle) diff --git a/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/AdoNetGrainStorageServiceCollectionExtensions.cs b/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/AdoNetGrainStorageServiceCollectionExtensions.cs index 103634aaf2..ec160a2507 100644 --- a/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/AdoNetGrainStorageServiceCollectionExtensions.cs +++ b/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/AdoNetGrainStorageServiceCollectionExtensions.cs @@ -61,6 +61,7 @@ public static IServiceCollection AddAdoNetGrainStorage(this IServiceCollection s configureOptions?.Invoke(services.AddOptions(name)); services.ConfigureNamedOptionForLogging(name); services.AddTransient, DefaultStorageProviderSerializerOptionsConfigurator>(); + services.AddTransient, DefaultAdoNetGrainStorageOptionsHashPickerConfigurator>(); services.AddTransient(sp => new AdoNetGrainStorageOptionsValidator(sp.GetRequiredService>().Get(name), name)); return services.AddGrainStorage(name, AdoNetGrainStorageFactory.Create); } diff --git a/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/JenkinsHash.cs b/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/JenkinsHash.cs new file mode 100644 index 0000000000..0ca944c52b --- /dev/null +++ b/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/JenkinsHash.cs @@ -0,0 +1,85 @@ +using System; + +namespace Orleans.Storage +{ + // Based on the version in http://home.comcast.net/~bretm/hash/7.html, which is based on that + // in http://burtleburtle.net/bob/hash/evahash.html. + // Note that we only use the version that takes three ulongs, which was written by the Orleans team. + // implementation restored from Orleans v3.7.2: https://github.com/dotnet/orleans/blob/b24e446abfd883f0e4ed614f5267eaa3331548dc/src/Orleans.Core.Abstractions/IDs/JenkinsHash.cs, + // trimmed and slightly optimized + internal static class JenkinsHash + { + private static void Mix(ref uint aa, ref uint bb, ref uint cc) + { + uint a = aa; + uint b = bb; + uint c = cc; + + a -= b; a -= c; a ^= (c >> 13); + b -= c; b -= a; b ^= (a << 8); + c -= a; c -= b; c ^= (b >> 13); + a -= b; a -= c; a ^= (c >> 12); + b -= c; b -= a; b ^= (a << 16); + c -= a; c -= b; c ^= (b >> 5); + a -= b; a -= c; a ^= (c >> 3); + b -= c; b -= a; b ^= (a << 10); + c -= a; c -= b; c ^= (b >> 15); + + aa = a; + bb = b; + cc = c; + } + + // This is the reference implementation of the Jenkins hash. + public static uint ComputeHash(ReadOnlySpan data) + { + int len = data.Length; + uint a = 0x9e3779b9; + uint b = a; + uint c = 0; + int i = 0; + + while (i <= len - 12) + { + a += (uint)data[i++] | + ((uint)data[i++] << 8) | + ((uint)data[i++] << 16) | + ((uint)data[i++] << 24); + b += (uint)data[i++] | + ((uint)data[i++] << 8) | + ((uint)data[i++] << 16) | + ((uint)data[i++] << 24); + c += (uint)data[i++] | + ((uint)data[i++] << 8) | + ((uint)data[i++] << 16) | + ((uint)data[i++] << 24); + Mix(ref a, ref b, ref c); + } + c += (uint)len; + if (i < len) + a += data[i++]; + if (i < len) + a += (uint)data[i++] << 8; + if (i < len) + a += (uint)data[i++] << 16; + if (i < len) + a += (uint)data[i++] << 24; + if (i < len) + b += (uint)data[i++]; + if (i < len) + b += (uint)data[i++] << 8; + if (i < len) + b += (uint)data[i++] << 16; + if (i < len) + b += (uint)data[i++] << 24; + if (i < len) + c += (uint)data[i++] << 8; + if (i < len) + c += (uint)data[i++] << 16; + if (i < len) + c += (uint)data[i++] << 24; + Mix(ref a, ref b, ref c); + return c; + } + } +} \ No newline at end of file diff --git a/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/Orleans3CompatibleHasher.cs b/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/Orleans3CompatibleHasher.cs new file mode 100644 index 0000000000..5490264f86 --- /dev/null +++ b/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/Orleans3CompatibleHasher.cs @@ -0,0 +1,29 @@ +using System; + +namespace Orleans.Storage +{ + /// + /// Orleans v3-compatible hasher implementation for non-string-only grain key ids. + /// + internal class Orleans3CompatibleHasher : IHasher + { + /// + /// + /// + public string Description { get; } = $"Orleans v3 hash function ({nameof(JenkinsHash)})."; + + /// + /// . + /// + public int Hash(byte[] data) => Hash(data.AsSpan()); + + /// + /// . + /// + public int Hash(ReadOnlySpan data) + { + // implementation restored from Orleans v3.7.2: https://github.com/dotnet/orleans/blob/b24e446abfd883f0e4ed614f5267eaa3331548dc/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/OrleansDefaultHasher.cs + return unchecked((int)JenkinsHash.ComputeHash(data)); + } + } +} \ No newline at end of file diff --git a/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/Orleans3CompatibleStorageHashPicker.cs b/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/Orleans3CompatibleStorageHashPicker.cs new file mode 100644 index 0000000000..3d7806705f --- /dev/null +++ b/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/Orleans3CompatibleStorageHashPicker.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Orleans.Runtime; + +namespace Orleans.Storage +{ + /// + /// Orleans v3-compatible hash picker implementation for Orleans v3 -> v7+ migration scenarios. + /// + public class Orleans3CompatibleStorageHashPicker : IStorageHasherPicker + { + private readonly Orleans3CompatibleHasher _nonStringHasher; + + /// + /// . + /// + public ICollection HashProviders { get; } + + /// + /// A constructor. + /// + public Orleans3CompatibleStorageHashPicker() + { + _nonStringHasher = new(); + HashProviders = [_nonStringHasher]; + } + + /// + /// . + /// + public IHasher PickHasher( + string serviceId, + string storageProviderInstanceName, + string grainType, + GrainId grainId, + IGrainState grainState, + string tag = null) + { + // string-only grain keys had special behaviour in Orleans v3 + if (grainId.TryGetIntegerKey(out _, out _) || grainId.TryGetGuidKey(out _, out _)) + return _nonStringHasher; + + // unable to cache hasher instances: content-aware behaviour, see hasher implementation for details + return new Orleans3CompatibleStringKeyHasher(_nonStringHasher, grainType); + } + } +} diff --git a/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/Orleans3CompatibleStringKeyHasher.cs b/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/Orleans3CompatibleStringKeyHasher.cs new file mode 100644 index 0000000000..0caa63b5e1 --- /dev/null +++ b/src/AdoNet/Orleans.Persistence.AdoNet/Storage/Provider/Orleans3CompatibleStringKeyHasher.cs @@ -0,0 +1,98 @@ +using System; +using System.Buffers; +using System.Text; + +namespace Orleans.Storage +{ + /// + /// Orleans v3-compatible hasher implementation for string-only grain key ids. + /// + internal class Orleans3CompatibleStringKeyHasher : IHasher + { + private readonly Orleans3CompatibleHasher _innerHasher; + private readonly string _grainType; + + public Orleans3CompatibleStringKeyHasher(Orleans3CompatibleHasher innerHasher, string grainType) + { + _innerHasher = innerHasher; + _grainType = grainType; + } + + /// + /// + /// + public string Description { get; } = $"Orleans v3 hash function ({nameof(JenkinsHash)})."; + + /// + /// . + /// + public int Hash(byte[] data) + { + // Orleans v3 treats string-only keys as integer keys with extension (AdoGrainKey.IsLongKey == true), + // so data must be extended for string-only grain keys. + // But AdoNetGrainStorage implementation also uses such code: + // ... + // var grainIdHash = HashPicker.PickHasher(serviceId, this.name, baseGrainType, grainReference, grainState).Hash(grainId.GetHashBytes()); + // var grainTypeHash = HashPicker.PickHasher(serviceId, this.name, baseGrainType, grainReference, grainState).Hash(Encoding.UTF8.GetBytes(baseGrainType)); + // ... + // PickHasher parameters are the same for both calls so we need to analyze data content to distinguish these cases. + // It doesn't word if string key is equal to grain type name, but we consider this edge case to be negligibly rare. + + if (IsGrainTypeName(data)) + return _innerHasher.Hash(data); + + var extendedLength = data.Length + 8; + + const int maxOnStack = 256; + byte[] rentedBuffer = null; + + // assuming code below never throws, so calling ArrayPool.Return without try/finally block for JIT optimization + + var buffer = extendedLength > maxOnStack + ? (rentedBuffer = ArrayPool.Shared.Rent(extendedLength)).AsSpan() + : stackalloc byte[maxOnStack]; + + buffer = buffer[..extendedLength]; + + data.AsSpan().CopyTo(buffer); + // buffer may contain arbitrary data, setting zeros in 'extension' segment + buffer[data.Length..].Clear(); + + var hash = _innerHasher.Hash(buffer); + + if (rentedBuffer is not null) + ArrayPool.Shared.Return(rentedBuffer); + + return hash; + } + + private bool IsGrainTypeName(byte[] data) + { + // at least 1 byte per char + if (data.Length < _grainType.Length) + return false; + + var grainTypeByteCount = Encoding.UTF8.GetByteCount(_grainType); + if (grainTypeByteCount != data.Length) + return false; + + const int maxOnStack = 256; + byte[] rentedBuffer = null; + + // assuming code below never throws, so calling ArrayPool.Return without try/finally block for JIT optimization + + var buffer = grainTypeByteCount > maxOnStack + ? (rentedBuffer = ArrayPool.Shared.Rent(grainTypeByteCount)).AsSpan() + : stackalloc byte[maxOnStack]; + + buffer = buffer[..grainTypeByteCount]; + + var bytesWritten = Encoding.UTF8.GetBytes(_grainType, buffer); + var isGrainType = buffer[..bytesWritten].SequenceEqual(data); + if (rentedBuffer is not null) + ArrayPool.Shared.Return(rentedBuffer); + + return isGrainType; + } + } +}