Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
91 changes: 78 additions & 13 deletions src/SmoFacade/Listener.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("AgDatabaseMove.Unit")]
Comment thread
AdityaNPL marked this conversation as resolved.
namespace AgDatabaseMove.SmoFacade
{
using System;
Expand Down Expand Up @@ -69,12 +70,12 @@ public Listener(SqlConnectionStringBuilder connectionStringBuilder, string crede
var secondaryNames = availabilityGroup.Replicas.Where(l => l != primaryName);

// Connect to each server instance
Primary = AgListenerNameToServer(ref connectionStringBuilder, primaryName, credentialName);
Primary = AgInstanceNameToServer(ref connectionStringBuilder, primaryName, credentialName);
AvailabilityGroup = Primary.AvailabilityGroups.Single(ag => ag.Name == availabilityGroup.Name);

_secondaries = new List<Server>();
foreach(var secondaryName in secondaryNames)
_secondaries.Add(AgListenerNameToServer(ref connectionStringBuilder,
_secondaries.Add(AgInstanceNameToServer(ref connectionStringBuilder,
secondaryName,
credentialName));
}
Expand Down Expand Up @@ -130,19 +131,83 @@ private static string AgListenerName(string dataSource)
return dotIndex >= 0 ? dataSource.Remove(dotIndex) : dataSource;
}

private static Server AgListenerNameToServer(ref SqlConnectionStringBuilder connBuilder, string agInstanceName,
private static Server AgInstanceNameToServer(ref SqlConnectionStringBuilder connBuilder, string agInstanceName,
string credentialName)
{
var parts = agInstanceName.Split('\\');
if(parts.Length == 1)
Comment thread
AdityaNPL marked this conversation as resolved.
connBuilder.DataSource = Dns.GetHostEntry(agInstanceName).HostName;
else if(parts.Length == 2)
// NamedInstances: chop instance name, resolve DNS, slap instance name back on!
connBuilder.DataSource = $"{Dns.GetHostEntry(parts[0]).HostName}\\{parts[1]}";
else
throw new ArgumentException($"agInstanceName param {agInstanceName} cannot be resolved by DNS");

return new Server(connBuilder.ToString(), credentialName);
try
{
connBuilder.DataSource = ResolveDnsHostNameForInstance(agInstanceName, connBuilder.DataSource);
return new Server(connBuilder.ToString(), credentialName);
}
catch (Exception e)
{
throw new ArgumentException($"agInstanceName param {agInstanceName} cannot be resolved by DNS", e);
}
}

/// <summary>
/// Resolves 'agReplicaInstanceName' to a FQDN
/// However on Unix OS, when 'val' in 'Dns.GetHostEntry(val)' is not a complete domain (i.e is just "abc", instead of "abc.def.com"), it fails intermittently
/// Therefore, if dns lookup on just the instance name fails, we retry after appending the domain fragments from the listener to the instance name
/// </summary>
/// <param name="agReplicaInstanceName"> The name for an instance within the AG (for which we are trying to get the FQDN)</param>
/// <param name="agListenerDomain"> The complete domain for the AG listener</param>
private static string ResolveDnsHostNameForInstance(string agReplicaInstanceName, string agListenerDomain)
{
// Sometimes instances and listeners have ports or named instances
// Therefore, we strip them off before calling DNS.GetHostEntry() and then add them back to the result
var (listenerDomain, listenerPortOrNamedInstance) = SplitDomainAndPort(agListenerDomain);
var (instanceName, instancePortOrNamedInstance) = SplitDomainAndPort(agReplicaInstanceName);
var preferredPortOrNamedInstance = GetPreferredPort(instancePortOrNamedInstance, listenerPortOrNamedInstance);

try
{
return $"{Dns.GetHostEntry(instanceName).HostName}{preferredPortOrNamedInstance}";
}
catch (System.Net.Sockets.SocketException)
{
// Re-try by appending the domain fragments from listener to the instance name
// However, we don't need the listener's "host name" (first fragment), so we need to strip that off
// eg: if listener is "abc.def.ghi" we want to append only ".def.ghi" to the instance name
var listenerDomainFragments = listenerDomain.Split('.');
listenerDomainFragments[0] = null;
var instanceDomain = $"{instanceName}{string.Join(".", listenerDomainFragments)}";

return $"{Dns.GetHostEntry(instanceDomain).HostName}{preferredPortOrNamedInstance}";
}
}

// First preference is to add back a port over a named instance
// (port is TCP/IP standard while named instance is only SQL Server standard)
// If both are same type, then prioritize instance over listener (in almost all cases they should be identical)
internal static string GetPreferredPort(string instancePortOrNamedInstance, string listenerPortOrNamedInstance)
{
if (string.IsNullOrEmpty(instancePortOrNamedInstance) || string.IsNullOrEmpty(listenerPortOrNamedInstance))
{
return (instancePortOrNamedInstance ?? listenerPortOrNamedInstance);
}
return instancePortOrNamedInstance.StartsWith("\\") && listenerPortOrNamedInstance.StartsWith(",")
? listenerPortOrNamedInstance
: instancePortOrNamedInstance;
}

// This function handles named instances ("<domain>\<named instance>") in the same way as ports
internal static (string domain, string port) SplitDomainAndPort(string domainAndPort)
{
Comment thread
AdityaNPL marked this conversation as resolved.
var domain = domainAndPort;
var splitValue = domainAndPort.Contains(',') ? "," : domainAndPort.Contains('\\') ? "\\" : null;

if(splitValue == null)
{
return (domain, null);
}

var fragments = domainAndPort.Split(splitValue.ToCharArray(), 2);
domain = fragments[0];
var port = $"{splitValue}{fragments[1]}";

return (domain, port);
}

}
}
84 changes: 84 additions & 0 deletions tests/AgDatabaseMove.Unit/SmoFacadeListenerTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System.Collections.Generic;
using Xunit;

namespace AgDatabaseMove.Unit
{
public class SmoFacadeListenerTest
{

/// <summary>
/// Tests for Listener.SplitDomainAndPort()
/// </summary>

private const string DOMAIN = "abc.def.ghi";
private const string HOST = "abc";
private const string PORT = ",123";
private const string NAMED_INSTANCE = "\\SQL";
private const string BAD_PORT = ":123";
private const string BAD_NAMED_INSTANCE = "SQL";

Comment thread
AdityaNPL marked this conversation as resolved.
// {input, expectedDomain, expectedPort (can be null)}
public static IEnumerable<object[]> ValidPorts => new List<object[]> {
new object[] { DOMAIN, DOMAIN, null },
new object[] { $"{DOMAIN}{PORT}", DOMAIN, PORT },
new object[] { $"{DOMAIN}{NAMED_INSTANCE}", DOMAIN, NAMED_INSTANCE },

new object[] { HOST, HOST, null },
new object[] { $"{HOST}{PORT}", HOST, PORT },
new object[] { $"{HOST}{NAMED_INSTANCE}", HOST, NAMED_INSTANCE }
};

[Theory, MemberData(nameof(ValidPorts))]
public void ValidPortTests(string input, string expectedDomain, string expectedPort=null)
{
var (domain, port) = SmoFacade.Listener.SplitDomainAndPort(input);
Assert.Equal(domain, expectedDomain);
Assert.Equal(port, expectedPort);
}

// {input, expectedDomain, expectedPort (can be null)}
public static IEnumerable<object[]> InvalidPorts => new List<object[]> {
new object[] { $"{DOMAIN}{BAD_PORT}", $"{DOMAIN}{BAD_PORT}", null },
new object[] { $"{DOMAIN}{BAD_NAMED_INSTANCE}", $"{DOMAIN}{BAD_NAMED_INSTANCE}", null },

new object[] { $"{HOST}{BAD_PORT}", $"{HOST}{BAD_PORT}", null },
new object[] { $"{HOST}{BAD_NAMED_INSTANCE}", $"{HOST}{BAD_NAMED_INSTANCE}", null },
};

[Theory, MemberData(nameof(InvalidPorts))]
public void InvalidPortTests(string input, string expectedDomain, string expectedPort=null)
{
var (domain, port) = SmoFacade.Listener.SplitDomainAndPort(input);
Assert.Equal(domain, expectedDomain);
Assert.Equal(port, expectedPort);
}


/// <summary>
/// Tests for Listener.GetPreferredPort()
/// </summary>

private const string IPort = ",123";
private const string INamed = "\\SQL";

private const string LPort = ",321";
private const string LNamed = "\\LQS";

[Theory]
[InlineData(null, null, null)]
[InlineData(null, LPort, LPort)]
[InlineData(null, LNamed, LNamed)]
[InlineData(IPort, null, IPort)]
[InlineData(IPort, LPort, IPort)]
[InlineData(IPort, LNamed, IPort)]
[InlineData(INamed, null, INamed)]
[InlineData(INamed, LPort, LPort)]
[InlineData(INamed, LNamed, INamed)]
public void PortPreferenceTests(string instancePortOrNamedInstance, string listenerPortOrNamedInstance, string expectedResult)
{
var preferredPortOrNamedInstance = SmoFacade.Listener.GetPreferredPort(instancePortOrNamedInstance, listenerPortOrNamedInstance);
Assert.Equal(preferredPortOrNamedInstance, expectedResult);
}

}
}