Skip to content

Instantly share code, notes, and snippets.

@bboyle1234
Created March 2, 2018 12:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bboyle1234/38e766d2f5baa3d94ac10bce30c04c3f to your computer and use it in GitHub Desktop.
Save bboyle1234/38e766d2f5baa3d94ac10bce30c04c3f to your computer and use it in GitHub Desktop.
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;
}
}
}
@bboyle1234
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment