Skip to content

Instantly share code, notes, and snippets.

@bboyle1234
Created March 2, 2018 12:05
Show Gist options
  • Select an option

  • Save bboyle1234/38e766d2f5baa3d94ac10bce30c04c3f to your computer and use it in GitHub Desktop.

Select an option

Save bboyle1234/38e766d2f5baa3d94ac10bce30c04c3f to your computer and use it in GitHub Desktop.

Revisions

  1. bboyle1234 created this gist Mar 2, 2018.
    222 changes: 222 additions & 0 deletions RedisStorage.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,222 @@
    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;
    }

    }
    }