Skip to content

Commit

Permalink
ADO.NET IHashPicker customization API + Orleans v3-compatible `IHas…
Browse files Browse the repository at this point in the history
…hPicker` 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
  • Loading branch information
vladislav-prishchepa authored Nov 12, 2024
1 parent 3535eb6 commit 77cb079
Show file tree
Hide file tree
Showing 7 changed files with 298 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Microsoft.Extensions.Options;
using Orleans.Persistence.AdoNet.Storage;
using Orleans.Runtime;
using Orleans.Storage;
Expand Down Expand Up @@ -37,6 +38,21 @@ public class AdoNetGrainStorageOptions : IStorageProviderSerializerOptions

/// <inheritdoc/>
public IGrainStorageSerializer GrainStorageSerializer { get; set; }

/// <summary>
/// Gets or sets the hasher picker to use for this storage provider.
/// </summary>
public IStorageHasherPicker HashPicker { get; set; }

/// <summary>
/// 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.
/// </summary>
public void UseOrleans3CompatibleHasher()
{
// content-aware hashing with different pickers, unable to use standard StorageHasherPicker
this.HashPicker = new Orleans3CompatibleStorageHashPicker();
}
}

/// <summary>
Expand Down Expand Up @@ -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.");
}
}
}

/// <summary>
/// Provides default configuration HashPicker for AdoNetGrainStorageOptions.
/// </summary>
public class DefaultAdoNetGrainStorageOptionsHashPickerConfigurator : IPostConfigureOptions<AdoNetGrainStorageOptions>
{
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() });
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public class AdoNetGrainStorage: IGrainStorage, ILifecycleParticipant<ISiloLifec
/// <summary>
/// The hash generator used to hash natural keys, grain ID and grain type to a more narrow index.
/// </summary>
public IStorageHasherPicker HashPicker { get; set; } = new StorageHasherPicker(new[] { new OrleansDefaultHasher() });
public IStorageHasherPicker HashPicker { get; set; }

private readonly AdoNetGrainStorageOptions options;
private readonly IProviderRuntime providerRuntime;
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public static IServiceCollection AddAdoNetGrainStorage(this IServiceCollection s
configureOptions?.Invoke(services.AddOptions<AdoNetGrainStorageOptions>(name));
services.ConfigureNamedOptionForLogging<AdoNetGrainStorageOptions>(name);
services.AddTransient<IPostConfigureOptions<AdoNetGrainStorageOptions>, DefaultStorageProviderSerializerOptionsConfigurator<AdoNetGrainStorageOptions>>();
services.AddTransient<IPostConfigureOptions<AdoNetGrainStorageOptions>, DefaultAdoNetGrainStorageOptionsHashPickerConfigurator>();
services.AddTransient<IConfigurationValidator>(sp => new AdoNetGrainStorageOptionsValidator(sp.GetRequiredService<IOptionsMonitor<AdoNetGrainStorageOptions>>().Get(name), name));
return services.AddGrainStorage(name, AdoNetGrainStorageFactory.Create);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<byte> 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;

namespace Orleans.Storage
{
/// <summary>
/// Orleans v3-compatible hasher implementation for non-string-only grain key ids.
/// </summary>
internal class Orleans3CompatibleHasher : IHasher
{
/// <summary>
/// <see cref="IHasher.Description"/>
/// </summary>
public string Description { get; } = $"Orleans v3 hash function ({nameof(JenkinsHash)}).";

/// <summary>
/// <see cref="IHasher.Hash(byte[])"/>.
/// </summary>
public int Hash(byte[] data) => Hash(data.AsSpan());

/// <summary>
/// <see cref="IHasher.Hash(byte[])"/>.
/// </summary>
public int Hash(ReadOnlySpan<byte> 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));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Collections.Generic;
using Orleans.Runtime;

namespace Orleans.Storage
{
/// <summary>
/// Orleans v3-compatible hash picker implementation for Orleans v3 -> v7+ migration scenarios.
/// </summary>
public class Orleans3CompatibleStorageHashPicker : IStorageHasherPicker
{
private readonly Orleans3CompatibleHasher _nonStringHasher;

/// <summary>
/// <see cref="IStorageHasherPicker.HashProviders"/>.
/// </summary>
public ICollection<IHasher> HashProviders { get; }

/// <summary>
/// A constructor.
/// </summary>
public Orleans3CompatibleStorageHashPicker()
{
_nonStringHasher = new();
HashProviders = [_nonStringHasher];
}

/// <summary>
/// <see cref="IStorageHasherPicker.PickHasher{T}"/>.
/// </summary>
public IHasher PickHasher<T>(
string serviceId,
string storageProviderInstanceName,
string grainType,
GrainId grainId,
IGrainState<T> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System;
using System.Buffers;
using System.Text;

namespace Orleans.Storage
{
/// <summary>
/// Orleans v3-compatible hasher implementation for string-only grain key ids.
/// </summary>
internal class Orleans3CompatibleStringKeyHasher : IHasher
{
private readonly Orleans3CompatibleHasher _innerHasher;
private readonly string _grainType;

public Orleans3CompatibleStringKeyHasher(Orleans3CompatibleHasher innerHasher, string grainType)
{
_innerHasher = innerHasher;
_grainType = grainType;
}

/// <summary>
/// <see cref="IHasher.Description"/>
/// </summary>
public string Description { get; } = $"Orleans v3 hash function ({nameof(JenkinsHash)}).";

/// <summary>
/// <see cref="IHasher.Hash(byte[])"/>.
/// </summary>
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<byte>.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<byte>.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<byte>.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<byte>.Shared.Return(rentedBuffer);

return isGrainType;
}
}
}

0 comments on commit 77cb079

Please sign in to comment.