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 1 commit
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
62 changes: 62 additions & 0 deletions src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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 avaialble 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;
}

internal override Task AfterConnect(ConnectionMultiplexer muxer, ConnectionMultiplexer.LogProxy logProxy)
{
if (!muxer.CommandMap.IsAvailable(RedisCommand.SUBSCRIBE))
{
return AzureMaintenanceEvent.AddListenerAsync(muxer, logProxy);
}
return Task.CompletedTask;
}
}
}
197 changes: 197 additions & 0 deletions src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
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
{
/// <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>
/// Known providers to match - this is intentionally modifiable for expert users and wrapper libraries.
/// </summary>
public static List<DefaultOptionsProvider> KnownProviders { get; set; } = new()
NickCraver marked this conversation as resolved.
Show resolved Hide resolved
{
new AzureOptionsProvider(),
new DefaultOptionsProvider()
};

/// <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 { get; internal set; }

/// <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);

// Note: this is statically backed because it doesn't change.
private static string defaultClientName;
/// <summary>
/// The default client name for a connection, with the library version appended.
/// </summary>
public virtual string ClientName =>
defaultClientName ??= (TryGetAzureRoleInstanceIdNoThrow()
philon-msft marked this conversation as resolved.
Show resolved Hide resolved
?? Environment.MachineName
?? Environment.GetEnvironmentVariable("ComputerName")
?? "StackExchange.Redis") + "(v" + Utils.GetLibVersion() + ")";

/// <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 compat/conveinence.
/// 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;
}

internal virtual Task AfterConnect(ConnectionMultiplexer muxer, ConnectionMultiplexer.LogProxy logProxy) => Task.CompletedTask;
}
}
Loading