csharp/1100100/Uragano/src/Uragano.Consul/ConsulServiceDiscovery.cs

ConsulServiceDiscovery.cs
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Consul;
using Microsoft.Extensions.Logging;
using Uragano.Abstractions;
using Uragano.Abstractions.ServiceDiscovery;
using Microsoft.Extensions.DependencyInjection;


namespace Uragano.Consul
{
    public clast ConsulServiceDiscovery : IServiceDiscovery
    {
        private ILogger Logger { get; }
        private ServerSettings ServerSettings { get; }

        private ConsulClientConfigure ConsulClientConfigure { get; }

        private ConsulRegisterServiceConfiguration ConsulRegisterServiceConfiguration { get; set; }

        private static readonly AsyncLock AsyncLock = new AsyncLock();

        private static readonly ConcurrentDictionary ServiceNodes = new ConcurrentDictionary();

        public event NodeLeaveHandler OnNodeLeave;
        public event NodeJoinHandler OnNodeJoin;
        public ConsulServiceDiscovery(UraganoSettings uraganoSettings, ILogger logger, IServiceDiscoveryClientConfiguration clientConfiguration, IServiceProvider service)
        {
            if (!(clientConfiguration is ConsulClientConfigure client))
                throw new ArgumentNullException(nameof(clientConfiguration));
            var agent = service.GetService();
            if (agent != null)
            {
                if (!(agent is ConsulRegisterServiceConfiguration serviceAgent))
                    throw new ArgumentNullException(nameof(ConsulRegisterServiceConfiguration));
                ConsulRegisterServiceConfiguration = serviceAgent;
            }


            ConsulClientConfigure = client;
            ServerSettings = uraganoSettings.ServerSettings;
            Logger = logger;
        }

        public async Task RegisterAsync(CancellationToken cancellationToken = default)
        {
            if (ConsulRegisterServiceConfiguration == null)
            {
                ConsulRegisterServiceConfiguration = new ConsulRegisterServiceConfiguration();
            }

            if (string.IsNullOrWhiteSpace(ConsulRegisterServiceConfiguration.Id))
            {
                ConsulRegisterServiceConfiguration.Id = ServerSettings.ToString();
            }

            if (string.IsNullOrWhiteSpace(ConsulRegisterServiceConfiguration.Name))
            {
                ConsulRegisterServiceConfiguration.Name = ServerSettings.ToString();
            }

            if (string.IsNullOrWhiteSpace(ConsulRegisterServiceConfiguration.Name))
                throw new ArgumentNullException(nameof(ConsulRegisterServiceConfiguration.Name), "Service name value cannot be null.");

            Logger.LogTrace("Start registering with consul[{0}]...", ConsulClientConfigure.Address);

            try
            {
                using (var consul = new ConsulClient(conf =>
                {
                    conf.Address = ConsulClientConfigure.Address;
                    conf.Datacenter = ConsulClientConfigure.Datacenter;
                    conf.Token = ConsulClientConfigure.Token;
                    conf.WaitTime = ConsulClientConfigure.WaitTime;
                }))
                {
                    ConsulRegisterServiceConfiguration.Meta = new Dictionary{
                        {"X-Tls",(ServerSettings.X509Certificate2!=null).ToString()}
                    };
                    if (ServerSettings.Weight.HasValue)
                    {
                        ConsulRegisterServiceConfiguration.Meta.Add("X-Weight", ServerSettings.Weight.ToString());
                    }

                    //Register service to consul agent 
                    var result = await consul.Agent.ServiceRegister(new AgentServiceRegistration
                    {
                        Address = ServerSettings.Address,
                        Port = ServerSettings.Port,
                        ID = ConsulRegisterServiceConfiguration.Id,
                        Name = ConsulRegisterServiceConfiguration.Name,
                        EnableTagOverride = ConsulRegisterServiceConfiguration.EnableTagOverride,
                        Meta = ConsulRegisterServiceConfiguration.Meta,
                        Tags = ConsulRegisterServiceConfiguration.Tags,
                        Check = new AgentServiceCheck
                        {
                            TCP = ServerSettings.ToString(),
                            DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(20),
                            Timeout = TimeSpan.FromSeconds(3),
                            Interval = ConsulRegisterServiceConfiguration.HealthCheckInterval
                        }
                    }, cancellationToken);
                    if (result.StatusCode != HttpStatusCode.OK)
                    {
                        Logger.LogError("Registration service failed:{0}", result.StatusCode);
                        throw new ConsulRequestException("Registration service failed.", result.StatusCode);
                    }

                    Logger.LogTrace("Consul service registration completed");
                    return result.StatusCode == HttpStatusCode.OK;
                }
            }
            catch (Exception e)
            {
                Logger.LogError(e, "Registration service failed:{0}", e.Message);
                return false;
            }
        }

        public async Task DeregisterAsync()
        {
            Logger.LogTrace("Start deregistration consul...");
            using (var consul = new ConsulClient(conf =>
            {
                conf.Address = ConsulClientConfigure.Address;
                conf.Datacenter = ConsulClientConfigure.Datacenter;
                conf.Token = ConsulClientConfigure.Token;
                conf.WaitTime = ConsulClientConfigure.WaitTime;
            }))
            {
                try
                {
                    var result = await consul.Agent.ServiceDeregister(ConsulRegisterServiceConfiguration.Id);
                    if (result.StatusCode != HttpStatusCode.OK)
                    {
                        Logger.LogError("Deregistration service failed:{0}", result.StatusCode);
                        throw new ConsulRequestException("Deregistration service failed.", result.StatusCode);
                    }

                    Logger.LogTrace("Deregistration consul has been completed.");
                    return result.StatusCode == HttpStatusCode.OK;
                }
                catch (Exception e)
                {
                    Logger.LogError(e, e.Message);
                    return false;
                }
            }
        }

        public async Task QueryServiceAsync(string serviceName, CancellationToken cancellationToken = default)
        {
            if (string.IsNullOrWhiteSpace(serviceName))
                throw new ArgumentNullException(nameof(serviceName));
            using (var consul = new ConsulClient(conf =>
            {
                conf.Address = ConsulClientConfigure.Address;
                conf.Datacenter = ConsulClientConfigure.Datacenter;
                conf.Token = ConsulClientConfigure.Token;
                conf.WaitTime = ConsulClientConfigure.WaitTime;
            }))
            {
                try
                {
                    var result = await consul.Health.Service(serviceName, "", true, cancellationToken);

                    if (result.StatusCode != HttpStatusCode.OK)
                    {
                        Logger.LogError("Query the service {0} failed:{0}", serviceName, result.StatusCode);
                        return new List();
                    }

                    if (!result.Response.Any())
                        return new List();

                    return result.Response.Select(p => new ServiceDiscoveryInfo(p.Service.ID, p.Service.Address, p.Service.Port, int.Parse(p.Service.Meta?.FirstOrDefault(m => m.Key == "X-Weight").Value ?? "0"), bool.Parse(p.Service.Meta?.FirstOrDefault(m => m.Key == "X-Tls").Value ?? "false"), p.Service.Meta)).ToList();
                }
                catch (Exception ex)
                {
                    Logger.LogError("Query the service {2} error:{0}\n{1}", ex.Message, ex.StackTrace, serviceName);
                    throw;
                }
            }
        }

        public IReadOnlyDictionary GetAllService()
        {
            return ServiceNodes.ToDictionary(k => k.Key, v => (IReadOnlyList)v.Value);
        }

        public async Task GetServiceNodes(string serviceName)
        {
            if (ServiceNodes.TryGetValue(serviceName, out var result))
                return result;
            var serviceNodes = await QueryServiceAsync(serviceName);
            if (!serviceNodes.Any())
            {
                return new List();
            }
            var nodes = serviceNodes.Select(p => new ServiceNodeInfo(p.ServiceId, p.Address, p.Port, p.Weight, p.EnableTls, p.Meta)).ToList();

            if (ServiceNodes.TryAdd(serviceName, nodes))
                return nodes;

            throw new InvalidOperationException($"Service {serviceName} not found.");
        }

        public async Task NodeMonitor(CancellationToken cancellationToken)
        {
            Logger.LogTrace("Start refresh service status,waiting for locking...");
            using (await AsyncLock.LockAsync(cancellationToken))
            {
                if (cancellationToken.IsCancellationRequested)
                    return;

                foreach (var service in ServiceNodes)
                {
                    Logger.LogTrace($"Service {service.Key} refreshing...");
                    try
                    {
                        var healthNodes = await QueryServiceAsync(service.Key, cancellationToken);
                        if (cancellationToken.IsCancellationRequested)
                            break;

                        var leavedNodes = service.Value.Where(p => healthNodes.All(a => a.ServiceId != p.ServiceId))
                            .Select(p => p.ServiceId).ToArray();
                        if (leavedNodes.Any())
                        {
                            //RemoveNode(service.Key, leavedNodes);
                            if (!ServiceNodes.TryGetValue(service.Key, out var services)) return;
                            services.RemoveAll(p => leavedNodes.Any(n => n == p.ServiceId));
                            OnNodeLeave?.Invoke(service.Key, leavedNodes);
                            Logger.LogTrace($"These nodes are gone:{string.Join(",", leavedNodes)}");
                        }

                        var addedNodes = healthNodes.Where(p =>
                                service.Value.All(e => e.ServiceId != p.ServiceId)).Select(p =>
                                new ServiceNodeInfo(p.ServiceId, p.Address, p.Port, p.Weight, p.EnableTls, p.Meta))
                            .ToList();

                        if (addedNodes.Any())
                        {
                            //AddNode(service.Key, addedNodes);
                            if (ServiceNodes.TryGetValue(service.Key, out var services))
                                services.AddRange(addedNodes);
                            else
                                ServiceNodes.TryAdd(service.Key, addedNodes);

                            OnNodeJoin?.Invoke(service.Key, addedNodes);

                            Logger.LogTrace(
                                $"New nodes added:{string.Join(",", addedNodes.Select(p => p.ServiceId))}");
                        }
                    }
                    catch
                    {
                        // ignored
                    }
                }
                Logger.LogTrace("Complete refresh.");
            }
        }
    }
}