✨ New service discovery system
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="dotnet-etcd" Version="8.0.1" />
|
||||
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.2" />
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||
<PackageReference Include="Google.Api.CommonProtos" Version="2.17.0" />
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using dotnet_etcd;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@@ -51,9 +53,12 @@ public static class Extensions
|
||||
// });
|
||||
|
||||
builder.Services.AddSingleton<IClock>(SystemClock.Instance);
|
||||
builder.Services.AddSingleton(new EtcdClient(builder.Configuration.GetConnectionString("Registrar")));
|
||||
builder.Services.AddSingleton<ServiceRegistrar>();
|
||||
builder.Services.AddHostedService<ServiceRegistrarHostedService>();
|
||||
|
||||
builder.AddNatsClient("queue");
|
||||
builder.AddRedisClient("cache", configureOptions: opts => { opts.AbortOnConnectFail = false; });
|
||||
builder.AddNatsClient("Queue");
|
||||
builder.AddRedisClient("Cache", configureOptions: opts => { opts.AbortOnConnectFail = false; });
|
||||
|
||||
// Setup cache service
|
||||
builder.Services.AddStackExchangeRedisCache(options =>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DysonNetwork.Shared.GeoIp;
|
||||
namespace DysonNetwork.Shared.Geometry;
|
||||
|
||||
public class GeoPoint
|
||||
{
|
||||
@@ -1,14 +1,14 @@
|
||||
using MaxMind.GeoIP2;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DysonNetwork.Shared.GeoIp;
|
||||
namespace DysonNetwork.Shared.Geometry;
|
||||
|
||||
public class GeoIpOptions
|
||||
{
|
||||
public string DatabasePath { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class GeoIpService(IOptions<GeoIpOptions> options)
|
||||
public class GeoService(IOptions<GeoIpOptions> options)
|
||||
{
|
||||
private readonly string _databasePath = options.Value.DatabasePath;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using DysonNetwork.Shared.GeoIp;
|
||||
using DysonNetwork.Shared.Geometry;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using NodaTime.Serialization.Protobuf;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Shared.GeoIp;
|
||||
using DysonNetwork.Shared.Geometry;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.Protobuf;
|
||||
|
||||
93
DysonNetwork.Shared/Registry/ServiceRegistrar.cs
Normal file
93
DysonNetwork.Shared/Registry/ServiceRegistrar.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using dotnet_etcd;
|
||||
using Etcdserverpb;
|
||||
using Google.Protobuf;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace DysonNetwork.Shared.Registry;
|
||||
|
||||
public class ServiceRegistrar(EtcdClient etcd)
|
||||
{
|
||||
private CancellationTokenSource? _cts;
|
||||
private string? _serviceKey;
|
||||
private long _leaseId;
|
||||
private long _ttlSeconds;
|
||||
|
||||
/// <summary>
|
||||
/// Register the service in etcd with a TTL lease.
|
||||
/// </summary>
|
||||
public async Task RegisterAsync(string serviceName, string servicePart, string instanceId, string host, int port, long ttlSeconds = 30)
|
||||
{
|
||||
_ttlSeconds = ttlSeconds;
|
||||
_serviceKey = $"/services/{serviceName}/${servicePart}/{instanceId}";
|
||||
var serviceValue = $"{host}:{port}";
|
||||
|
||||
// Create and store TTL lease
|
||||
var leaseResp = await etcd.LeaseGrantAsync(new LeaseGrantRequest { TTL = _ttlSeconds });
|
||||
_leaseId = leaseResp.ID;
|
||||
|
||||
await etcd.PutAsync(new PutRequest()
|
||||
{
|
||||
Key = ByteString.CopyFromUtf8(_serviceKey),
|
||||
Value = ByteString.CopyFromUtf8(serviceValue),
|
||||
Lease = _leaseId
|
||||
});
|
||||
|
||||
// Start a background task to keep the lease alive
|
||||
_cts = new CancellationTokenSource();
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
while (!_cts.Token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await etcd.LeaseKeepAlive(_leaseId, _cts.Token);
|
||||
await Task.Delay(TimeSpan.FromSeconds(_ttlSeconds / 2), _cts.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error keeping lease alive: {ex.Message}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, _cts.Token);
|
||||
}
|
||||
|
||||
|
||||
public async Task DeregisterAsync()
|
||||
{
|
||||
await _cts?.CancelAsync();
|
||||
|
||||
if (_serviceKey != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await etcd.DeleteAsync(_serviceKey);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore delete errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ServiceRegistrarHostedService(ServiceRegistrar registrar, IConfiguration config) : IHostedService
|
||||
{
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var name = config["Service:Name"];
|
||||
var host = config["Service:Host"];
|
||||
var grpcPort = int.Parse(config["Service:GrpcPort"]!);
|
||||
var httpPort = int.Parse(config["Service:HttpPort"]!);
|
||||
var instanceId = config["Service:InstanceId"] ?? Guid.NewGuid().ToString("N");
|
||||
|
||||
await registrar.RegisterAsync(name, "grpc", instanceId, host, grpcPort);
|
||||
await registrar.RegisterAsync(name, "http", instanceId, host, httpPort);
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await registrar.DeregisterAsync();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user