Created
March 2, 2018 12:05
-
-
Save bboyle1234/38e766d2f5baa3d94ac10bce30c04c3f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.Extensions.DependencyInjection; | |
using Microsoft.Extensions.DependencyInjection.Extensions; | |
using Microsoft.Extensions.Logging; | |
using Microsoft.Extensions.Options; | |
using Newtonsoft.Json; | |
using Orleans; | |
using Orleans.Configuration; | |
using Orleans.Hosting; | |
using Orleans.Providers; | |
using Orleans.Runtime; | |
using Orleans.Serialization; | |
using Orleans.Storage; | |
using StackExchange.Redis; | |
using System; | |
using System.Collections.Generic; | |
using System.Text; | |
using System.Threading.Tasks; | |
namespace Silo { | |
public static class RedisStorageSiloBuilderExtensions { | |
public static ISiloHostBuilder AddRedisStorageAsDefault(this ISiloHostBuilder builder, Action<RedisStorageOptions> configureOptions) { | |
return builder.AddRedisStorage(ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME, configureOptions); | |
} | |
public static ISiloHostBuilder AddRedisStorage(this ISiloHostBuilder builder, string name, Action<RedisStorageOptions> configureOptions) { | |
return builder.ConfigureServices(services => services.AddRedisStorage(name, configureOptions)); | |
} | |
public static ISiloHostBuilder AddRedisStorageAsDefault(this ISiloHostBuilder builder, Action<OptionsBuilder<RedisStorageOptions>> configureOptions = null) { | |
return builder.AddRedisStorage(ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME, configureOptions); | |
} | |
public static ISiloHostBuilder AddRedisStorage(this ISiloHostBuilder builder, string name, Action<OptionsBuilder<RedisStorageOptions>> configureOptions = null) { | |
return builder.ConfigureServices(services => services.AddRedisStorage(name, configureOptions)); | |
} | |
public static IServiceCollection AddRedisStorageAsDefault(this IServiceCollection services, Action<RedisStorageOptions> configureOptions) { | |
return services.AddRedisStorage(ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME, ob => ob.Configure(configureOptions)); | |
} | |
public static IServiceCollection AddRedisStorage(this IServiceCollection services, string name, Action<RedisStorageOptions> configureOptions) { | |
return services.AddRedisStorage(name, ob => ob.Configure(configureOptions)); | |
} | |
public static IServiceCollection AddRedisStorageAsDefault(this IServiceCollection services, Action<OptionsBuilder<RedisStorageOptions>> configureOptions = null) { | |
return services.AddRedisStorage(ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME, configureOptions); | |
} | |
public static IServiceCollection AddRedisStorage(this IServiceCollection services, string name, Action<OptionsBuilder<RedisStorageOptions>> configureOptions = null) { | |
configureOptions?.Invoke(services.AddOptions<RedisStorageOptions>(name)); | |
services.ConfigureNamedOptionForLogging<RedisStorageOptions>(name); | |
services.TryAddSingleton<IGrainStorage>(sp => sp.GetServiceByName<IGrainStorage>(ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME)); | |
return services.AddSingletonNamedService<IGrainStorage>(name, RedisStorageFactory.Create); | |
} | |
} | |
public class RedisStorageFactory { | |
public RedisStorageFactory() { } | |
public static IGrainStorage Create(IServiceProvider services, string name) { | |
return ActivatorUtilities.CreateInstance<RedisStorage>(services, services.GetRequiredService<IOptionsSnapshot<RedisStorageOptions>>().Get(name), name); | |
} | |
} | |
public class RedisStorageOptions { | |
public string ConnectionString { get; set; } = "localhost"; | |
public bool UseJsonFormat { get; set; } = true; | |
public int DatabaseNumber { get; set; } = -1; | |
} | |
// TODO: No idea how/when this gets used, or how to plug it in. | |
public class RedisStorageOptionsValidator : IConfigurationValidator { | |
readonly RedisStorageOptions options; | |
readonly string name; | |
public RedisStorageOptionsValidator(RedisStorageOptions options, string name) { | |
this.options = options; | |
this.name = name; | |
} | |
public void ValidateConfiguration() { | |
// TODO: | |
} | |
} | |
// I inherited IProvider in the hope that the asnyc "Init" method could be used for time-consuming redis connection operations. | |
// But wasn't able to get the orleans system to call the Init or the Close method. | |
public class RedisStorage : IGrainStorage, IProvider { | |
readonly string Name; | |
readonly ILogger Logger; | |
readonly RedisStorageOptions Options; | |
readonly SerializationManager SerializationManager; | |
ConnectionMultiplexer connectionMultiplexer; | |
IDatabase redisDatabase; | |
JsonSerializerSettings jsonSettings; | |
public RedisStorage(string name, IServiceProvider serviceProvider, RedisStorageOptions options, ILoggerFactory loggerFactory) { | |
Name = name; | |
Options = options; | |
Logger = loggerFactory.CreateLogger<RedisStorageOptions>(); | |
SerializationManager = serviceProvider.GetRequiredService<SerializationManager>(); | |
// I'd rather put this in an async "Init" method. But I had to move it to the constructor because | |
// the "Init" method doesn't get called. | |
connectionMultiplexer = ConnectionMultiplexer.ConnectAsync(Options.ConnectionString).Result; | |
redisDatabase = connectionMultiplexer.GetDatabase(Options.DatabaseNumber); | |
if (Options.UseJsonFormat) { | |
jsonSettings = new Newtonsoft.Json.JsonSerializerSettings() { | |
TypeNameHandling = TypeNameHandling.All, | |
PreserveReferencesHandling = PreserveReferencesHandling.Objects, | |
DateFormatHandling = DateFormatHandling.IsoDateFormat, | |
DefaultValueHandling = DefaultValueHandling.Ignore, | |
MissingMemberHandling = MissingMemberHandling.Ignore, | |
NullValueHandling = NullValueHandling.Ignore, | |
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, | |
}; | |
} | |
} | |
string IProvider.Name => Name; | |
public async Task Init(string name, IProviderRuntime providerRuntime, IProviderConfiguration config) { | |
await Task.CompletedTask; | |
// This stuff was moved to the constructor because I couldn't get Orleans to call this Init method | |
//connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(Options.ConnectionString); | |
//redisDatabase = connectionMultiplexer.GetDatabase(Options.DatabaseNumber); | |
//if (Options.UseJsonFormat) { | |
// jsonSettings = new Newtonsoft.Json.JsonSerializerSettings() { | |
// TypeNameHandling = TypeNameHandling.All, | |
// PreserveReferencesHandling = PreserveReferencesHandling.Objects, | |
// DateFormatHandling = DateFormatHandling.IsoDateFormat, | |
// DefaultValueHandling = DefaultValueHandling.Ignore, | |
// MissingMemberHandling = MissingMemberHandling.Ignore, | |
// NullValueHandling = NullValueHandling.Ignore, | |
// ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, | |
// }; | |
//} | |
} | |
// TODO: I can't get Orleans to call this. It needs to be called when the silo shuts down. | |
public Task Close() { | |
connectionMultiplexer.Dispose(); | |
return Task.CompletedTask; | |
} | |
public Task ClearStateAsync(string grainType, GrainReference grainReference, IGrainState grainState) { | |
var key = grainReference.ToKeyString(); | |
if (Logger.IsEnabled(LogLevel.Trace)) { | |
Logger.Trace((int)ProviderErrorCode.RedisStorageProvider_ClearingData, "Clearing: GrainType={0} Pk={1} Grainid={2} ETag={3} to Database={4}", | |
grainType, key, grainReference, grainState.ETag, redisDatabase.Database); | |
} | |
return redisDatabase.KeyDeleteAsync(key); | |
} | |
public async Task ReadStateAsync(string grainType, GrainReference grainReference, IGrainState grainState) { | |
var primaryKey = grainReference.ToKeyString(); | |
if (Logger.IsEnabled(LogLevel.Trace)) { | |
Logger.Trace((int)ProviderErrorCode.RedisStorageProvider_ReadingData, "Reading: GrainType={0} Pk={1} Grainid={2} from Database={3}", | |
grainType, primaryKey, grainReference, redisDatabase.Database); | |
} | |
Envelope data = null; | |
RedisValue value = await redisDatabase.StringGetAsync(primaryKey); | |
if (value.HasValue) { | |
if (Options.UseJsonFormat) { | |
// jsonSettings includes $typeName in the serialization, so there's no problem extracting the correct data.State object type. | |
data = JsonConvert.DeserializeObject<Envelope>(value, jsonSettings); | |
grainState.State = data.State; | |
grainState.ETag = data.eTag; | |
} else { | |
data = SerializationManager.DeserializeFromByteArray<Envelope>(value); | |
grainState.State = data.State; | |
grainState.ETag = data.eTag; | |
} | |
} | |
} | |
public async Task WriteStateAsync(string grainType, GrainReference grainReference, IGrainState grainState) { | |
var key = grainReference.ToKeyString(); | |
if (Logger.IsEnabled(LogLevel.Trace)) { | |
Logger.Trace((int)ProviderErrorCode.RedisStorageProvider_WritingData, "Writing: GrainType={0} PrimaryKey={1} Grainid={2} ETag={3} to Database={4}", | |
grainType, key, grainReference, grainState.ETag, redisDatabase.Database); | |
} | |
//var data = grainState.State; | |
var data = new Envelope { | |
eTag = Guid.NewGuid().ToString("N"), | |
State = grainState.State, | |
}; | |
if (Options.UseJsonFormat) { | |
var payload = JsonConvert.SerializeObject(data, jsonSettings); | |
await redisDatabase.StringSetAsync(key, payload); | |
} else { | |
byte[] payload = SerializationManager.SerializeToByteArray(data); | |
await redisDatabase.StringSetAsync(key, payload); | |
} | |
grainState.ETag = data.eTag; | |
} | |
internal enum ProviderErrorCode { | |
RedisProviderBase = 300000, | |
RedisStorageprovider_ProviderName = RedisProviderBase + 200, | |
RedisStorageProvider_ReadingData = RedisProviderBase + 300, | |
RedisStorageProvider_WritingData = RedisProviderBase + 400, | |
RedisStorageProvider_ClearingData = RedisProviderBase + 500 | |
} | |
class Envelope { | |
public string eTag; | |
public object State; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Issues: I can't get the options validation to work, and I can't get Orleans to call the Init and Close methods in the provider.