Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configuration: An idea on separation #1987

Merged
merged 17 commits into from
Mar 3, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using StackExchange.Redis.Maintenance;

namespace StackExchange.Redis.Configuration
{
/// <summary>
/// Options provider for Azure environments.
/// </summary>
public class AzureOptionsProvider : DefaultOptionsProvider
{
/// <summary>
/// Allow connecting after startup, in the cases where remote cache isn't ready or is overloaded.
/// </summary>
public override bool AbortOnConnectFail => false;

/// <summary>
/// The minimum version of Redis in Azure is 4, so use the widest set of available commands when connecting.
/// </summary>
public override Version DefaultVersion => RedisFeatures.v4_0_0;

/// <summary>
/// List of domains known to be Azure Redis, so we can light up some helpful functionality
/// for minimizing downtime during maintenance events and such.
/// </summary>
private static readonly List<string> azureRedisDomains = new()
NickCraver marked this conversation as resolved.
Show resolved Hide resolved
{
".redis.cache.windows.net",
".redis.cache.chinacloudapi.cn",
".redis.cache.usgovcloudapi.net",
".redis.cache.cloudapi.de",
".redisenterprise.cache.azure.net",
};

/// <inheritdoc/>
public override bool IsMatch(EndPoint endpoint)
{
if (endpoint is DnsEndPoint dnsEp)
{
foreach (var host in azureRedisDomains)
{
if (dnsEp.Host.EndsWith(host, StringComparison.InvariantCultureIgnoreCase))
philon-msft marked this conversation as resolved.
Show resolved Hide resolved
{
return true;
}
}
}
return false;
}

/// <inheritdoc/>
public override Task AfterConnectAsync(ConnectionMultiplexer muxer, Action<string> log)
=> AzureMaintenanceEvent.AddListenerAsync(muxer, log);
}
}
220 changes: 220 additions & 0 deletions src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Reflection;
using System.Threading.Tasks;

namespace StackExchange.Redis.Configuration
{
/// <summary>
/// A defaults providers for <see cref="ConfigurationOptions"/>.
/// This providers defaults not explicitly specified and is present to be inherited by environments that want to provide
/// better defaults for their use case, e.g. in a single wrapper library used many places.
/// </summary>
/// <remarks>
/// Why not just have a default <see cref="ConfigurationOptions"/> instance? Good question!
/// Since we null coalesce down to the defaults, there's an inherent pit-of-failure with that approach of <see cref="StackOverflowException"/>.
/// If you forget anything or if someone creates a provider nulling these out...kaboom.
/// </remarks>
public class DefaultOptionsProvider
{
private static readonly List<DefaultOptionsProvider> BuiltInProviders = new()
{
new AzureOptionsProvider()
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the one and only time this field is used is: in the KnownInitializers static field initializer; maybe just move this directly inline there?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had it this way before but it's a new nasty/confusing with the shortened new syntax it looked harder to understand, IMO - lemme add some comments!

private static LinkedList<DefaultOptionsProvider> KnownProviders { get; } = new (BuiltInProviders);

/// <summary>
/// Adds a provider to match endpoints against. The last provider added has the highest priority.
/// If you want your provider to match everything, implement <see cref="IsMatch(EndPoint)"/> as <c>return true;</c>.
/// </summary>
/// <param name="provider">The provider to add.</param>
public static void AddProvider(DefaultOptionsProvider provider) => KnownProviders.AddFirst(provider);

/// <summary>
/// Whether this options provider matches a given endpoint, for automatically selecting a provider based on what's being connected to.
/// </summary>
public virtual bool IsMatch(EndPoint endpoint) => false;

/// <summary>
/// Gets a provider for the given endpoints, falling back to <see cref="DefaultOptionsProvider"/> if nothing more specific is found.
/// </summary>
internal static Func<EndPointCollection, DefaultOptionsProvider> GetForEndpoints { get; } = (endpoints) =>
{
foreach (var endpoint in endpoints)
philon-msft marked this conversation as resolved.
Show resolved Hide resolved
{
foreach (var provider in KnownProviders)
{
if (provider.IsMatch(endpoint))
{
return provider;
}
}
}

return new DefaultOptionsProvider();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(thought about whether to singleton this, but ... meh, it doesn't matter; leave alone, I guess - it'll be collectable that way)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subtle thing below in that defaultClientName does have state, that's why newing up here!

};

/// <summary>
/// Gets or sets whether connect/configuration timeouts should be explicitly notified via a TimeoutException.
/// </summary>
public virtual bool AbortOnConnectFail => true;

/// <summary>
/// Indicates whether admin operations should be allowed.
/// </summary>
public virtual bool AllowAdmin => false;

/// <summary>
/// The backlog policy to be used for commands when a connection is unhealthy.
/// </summary>
public virtual BacklogPolicy BacklogPolicy => BacklogPolicy.Default;

/// <summary>
/// A Boolean value that specifies whether the certificate revocation list is checked during authentication.
/// </summary>
public virtual bool CheckCertificateRevocation => true;

/// <summary>
/// The number of times to repeat the initial connect cycle if no servers respond promptly.
/// </summary>
public virtual int ConnectRetry => 3;

/// <summary>
/// Specifies the time that should be allowed for connection.
/// Falls back to Max(5000, SyncTimeout) if null.
/// </summary>
public virtual TimeSpan? ConnectTimeout => null;

/// <summary>
/// The command-map associated with this configuration.
/// </summary>
public virtual CommandMap CommandMap => null;

/// <summary>
/// Channel to use for broadcasting and listening for configuration change notification.
/// </summary>
public virtual string ConfigurationChannel => "__Booksleeve_MasterChanged";

/// <summary>
/// The server version to assume.
/// </summary>
public virtual Version DefaultVersion => RedisFeatures.v3_0_0;

/// <summary>
/// Specifies the time interval at which connections should be pinged to ensure validity.
/// </summary>
public virtual TimeSpan KeepAliveInterval => TimeSpan.FromSeconds(60);

/// <summary>
/// Type of proxy to use (if any); for example <see cref="Proxy.Twemproxy"/>.
/// </summary>
public virtual Proxy Proxy => Proxy.None;

/// <summary>
/// The retry policy to be used for connection reconnects.
/// </summary>
public virtual IReconnectRetryPolicy ReconnectRetryPolicy => null;

/// <summary>
/// Indicates whether endpoints should be resolved via DNS before connecting.
/// If enabled the ConnectionMultiplexer will not re-resolve DNS when attempting to re-connect after a connection failure.
/// </summary>
public virtual bool ResolveDns => false;

/// <summary>
/// Specifies the time that the system should allow for synchronous operations.
/// </summary>
public virtual TimeSpan SyncTimeout => TimeSpan.FromSeconds(5);

/// <summary>
/// Tie-breaker used to choose between masters (must match the endpoint exactly).
/// </summary>
public virtual string TieBreaker => "__Booksleeve_TieBreak";

/// <summary>
/// Check configuration every n interval.
/// </summary>
public virtual TimeSpan ConfigCheckInterval => TimeSpan.FromMinutes(1);

// We memoize this to reduce cost on re-access
private string defaultClientName;
/// <summary>
/// The default client name for a connection, with the library version appended.
/// </summary>
public string ClientName => defaultClientName ??= GetDefaultClientName();

/// <summary>
/// Gets the default client name for a connection.
/// </summary>
protected virtual string GetDefaultClientName() =>
(TryGetAzureRoleInstanceIdNoThrow()
?? ComputerName
?? "StackExchange.Redis") + "(SE.Redis-v" + LibraryVersion + ")";

/// <summary>
/// String version of the StackExchange.Redis library, for use in any options.
/// </summary>
protected static string LibraryVersion => Utils.GetLibVersion();

/// <summary>
/// Name of the machine we're running on, for use in any options.
/// </summary>
protected static string ComputerName => Environment.MachineName ?? Environment.GetEnvironmentVariable("ComputerName");

/// <summary>
/// Tries to get the RoleInstance Id if Microsoft.WindowsAzure.ServiceRuntime is loaded.
/// In case of any failure, swallows the exception and returns null.
/// </summary>
/// <remarks>
/// Azure, in the default provider? Yes, to maintain existing compatibility/convenience.
/// Source != destination here.
/// </remarks>
internal static string TryGetAzureRoleInstanceIdNoThrow()
{
string roleInstanceId;
try
{
Assembly asm = null;
foreach (var asmb in AppDomain.CurrentDomain.GetAssemblies())
{
if (asmb.GetName().Name.Equals("Microsoft.WindowsAzure.ServiceRuntime"))
{
asm = asmb;
break;
}
}
if (asm == null)
return null;

var type = asm.GetType("Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment");

// https://msdn.microsoft.com/en-us/library/microsoft.windowsazure.serviceruntime.roleenvironment.isavailable.aspx
if (!(bool)type.GetProperty("IsAvailable").GetValue(null, null))
return null;

var currentRoleInstanceProp = type.GetProperty("CurrentRoleInstance");
var currentRoleInstanceId = currentRoleInstanceProp.GetValue(null, null);
roleInstanceId = currentRoleInstanceId.GetType().GetProperty("Id").GetValue(currentRoleInstanceId, null).ToString();

if (string.IsNullOrEmpty(roleInstanceId))
{
roleInstanceId = null;
}
}
catch (Exception)
{
//silently ignores the exception
roleInstanceId = null;
}
return roleInstanceId;
}

/// <summary>
/// The action to perform, if any, immediately after an initial connection completes.
/// </summary>
/// <param name="multiplexer">The multiplexer that just connected.</param>
/// <param name="log">The logger for the connection, to emit to the connection output log.</param>
public virtual Task AfterConnectAsync(ConnectionMultiplexer multiplexer, Action<string> log) => Task.CompletedTask;
}
}
Loading