< Summary - AsyncResponse (Release / net8.0+net10.0 / unit+integration)

Information
Class: AsyncResponse.Channels.Redis.RedisClusterNodeTable
Assembly: AsyncResponse.Channels.Redis
File(s): /_/src/Channels/AsyncResponse.Channels.Redis/RedisClusterNodeTable.cs
Line coverage
98%
Covered lines: 54
Uncovered lines: 1
Coverable lines: 55
Total lines: 142
Line coverage: 98.1%
Branch coverage
97%
Covered branches: 45
Total branches: 46
Branch coverage: 97.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Address()100%11100%
get_IsSlotOwner()100%22100%
Parse(...)95%202096.15%
TryReadAsync()100%44100%
OwnsNoSlots(...)100%44100%
IsSameNode(...)100%1212100%
SameHost(...)100%44100%

File(s)

/_/src/Channels/AsyncResponse.Channels.Redis/RedisClusterNodeTable.cs

#LineLine coverage
 1using Microsoft.Extensions.Logging;
 2using StackExchange.Redis;
 3using System.Globalization;
 4using System.Net;
 5
 6namespace AsyncResponse.Channels.Redis;
 7
 8/// <summary>
 9/// The slice of a <c>CLUSTER NODES</c> reply the recovery scan and the liveness probe need: which
 10/// address each node is reachable at, and whether it owns slots as a primary. Parsed from the raw
 11/// reply — the text format is Redis's own wire contract — rather than read through the client's
 12/// <c>ClusterConfiguration</c>, which cannot be constructed outside the client and would leave
 13/// this classification untestable.
 14/// <para>
 15/// One line per node: <c>id ip:port@cport[,hostname] flags master-id ping pong epoch link-state
 16/// [slot ...]</c>. Redis 3 omits <c>@cport</c>; an IPv6 address carries colons of its own, so the
 17/// port is whatever follows the LAST colon.
 18/// </para>
 19/// </summary>
 20internal static class RedisClusterNodeTable
 21{
 25022    internal readonly record struct Node(string Address, string? HostName, int Port, bool IsReplica, bool HasSlots)
 23    {
 24        /// <summary>
 25        /// A primary with a slot field of any kind — a migration marker counts: a primary that is
 26        /// importing its first slot already holds the keys moved into it so far.
 27        /// </summary>
 2428        public bool IsSlotOwner => !IsReplica && HasSlots;
 29    }
 30
 31    private const int FirstSlotField = 8;
 32
 33    internal static List<Node> Parse(string nodeTable)
 34    {
 835        var nodes = new List<Node>();
 6836        foreach (var line in nodeTable.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntrie
 37        {
 2638            var fields = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
 2639            if (fields.Length < 3)
 40                continue;
 41
 2442            var address = fields[1];
 2443            string? hostName = null;
 2444            if (address.IndexOf(',') is var comma and >= 0)
 45            {
 246                hostName = address[(comma + 1)..];
 247                address = address[..comma];
 48            }
 49
 2450            if (address.IndexOf('@') is var bus and >= 0)
 2051                address = address[..bus];
 52
 2453            var hasSlots = fields.Length > FirstSlotField;
 2454            var portSeparator = address.LastIndexOf(':');
 2455            var port = 0;
 2456            if (portSeparator <= 0
 2457                || !int.TryParse(address.AsSpan(portSeparator + 1), NumberStyles.None, CultureInfo.InvariantCulture, out
 58            {
 59                // No usable address (the noaddr flag prints ":0@0"). A line that lists slots is
 60                // kept anyway, under an address no endpoint can match: dropped, a slot owner
 61                // nobody can reach would vanish from the table, and a coverage check over the
 62                // rest would pass without its shard.
 263                if (!hasSlots)
 64                    continue;
 65
 066                (address, portSeparator, port) = (string.Empty, 0, 0);
 67            }
 68
 2269            var flags = fields[2].Split(',');
 2270            nodes.Add(new Node(
 2271                address[..portSeparator],
 2272                string.IsNullOrEmpty(hostName) ? null : hostName,
 2273                port,
 3074                IsReplica: Array.Exists(flags, static flag => flag is "slave" or "replica"),
 2275                HasSlots: hasSlots));
 76        }
 77
 878        return nodes;
 79    }
 80
 81    /// <summary>
 82    /// Reads and parses <paramref name="clusterNode"/>'s view of the node table, or <c>null</c>
 83    /// when it cannot be read or lists nothing — which every caller treats as "unknown", never as
 84    /// "no slot owners".
 85    /// </summary>
 86    internal static async Task<List<Node>?> TryReadAsync(IServer clusterNode, ILogger logger)
 87    {
 88        string? nodeTable;
 89        try
 90        {
 2091            nodeTable = await clusterNode.ClusterNodesRawAsync().ConfigureAwait(false);
 1892        }
 293        catch (Exception ex) when (ex is RedisException or TimeoutException or InvalidOperationException)
 94        {
 295            logger.LogDebug(ex, "Could not read CLUSTER NODES from {EndPoint}.", clusterNode.EndPoint);
 296            return null;
 97        }
 98
 1899        if (string.IsNullOrWhiteSpace(nodeTable))
 12100            return null;
 101
 6102        var nodes = Parse(nodeTable);
 6103        return nodes.Count == 0 ? null : nodes;
 20104    }
 105
 106    /// <summary>
 107    /// <c>true</c> only when the table LISTS <paramref name="endPoint"/> and lists it as a replica
 108    /// or as a node without slots. An endpoint the table does not list (a DNS name the cluster
 109    /// does not announce, a stale configuration entry) is unknown, and unknown is not "owns
 110    /// nothing".
 111    /// </summary>
 112    internal static bool OwnsNoSlots(List<Node> nodes, EndPoint? endPoint)
 113    {
 210114        foreach (var node in nodes)
 115        {
 88116            if (IsSameNode(node, endPoint))
 18117                return !node.IsSlotOwner;
 118        }
 119
 8120        return false;
 18121    }
 122
 123    /// <summary>Whether <paramref name="endPoint"/> is the address (or announced hostname) and port the table lists <pa
 124    internal static bool IsSameNode(Node node, EndPoint? endPoint)
 125    {
 94126        var (host, port) = endPoint switch
 94127        {
 72128            IPEndPoint ip => (ip.Address.ToString(), ip.Port),
 12129            DnsEndPoint dns => (dns.Host, dns.Port),
 10130            _ => (null, 0)
 94131        };
 132
 94133        return host is not null
 94134            && node.Port == port
 94135            && (SameHost(node.Address, host) || (node.HostName is { } announced && SameHost(announced, host)));
 136    }
 137
 138    private static bool SameHost(string left, string right)
 88139        => IPAddress.TryParse(left, out var leftAddress) && IPAddress.TryParse(right, out var rightAddress)
 88140            ? leftAddress.Equals(rightAddress)
 88141            : string.Equals(left, right, StringComparison.OrdinalIgnoreCase);
 142}