Switch Authentication to asynchronous streaming calls (#16)

* add base grpc service and swap auth service to streaming

* remove Authorize from hub itself

* remove unused usings

* heave files server to net 7, add exception handling in grpc auth stream

Co-authored-by: rootdarkarchon <root.darkarchon@outlook.com>
This commit is contained in:
rootdarkarchon
2022-10-13 16:55:23 +02:00
committed by GitHub
parent d37c1208fe
commit c98e2b2dd6
20 changed files with 313 additions and 159 deletions

View File

@@ -4,12 +4,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System;
using Microsoft.AspNetCore.SignalR;
using System.Globalization;
using MareSynchronos.API;
using MareSynchronosServer.Utils;
using System.Security.Claims;
using Microsoft.Extensions.Logging;
namespace MareSynchronosServer.Hubs;

View File

@@ -3,7 +3,6 @@ using System.Linq;
using System.Threading.Tasks;
using MareSynchronos.API;
using MareSynchronosServer.Utils;
using MareSynchronosShared.Authentication;
using MareSynchronosShared.Metrics;
using MareSynchronosShared.Models;
using MareSynchronosShared.Protos;
@@ -38,7 +37,7 @@ public partial class MareHub
await Task.Delay(1000).ConfigureAwait(false);
}
await _authServiceClient.RemoveAuthAsync(new RemoveAuthRequest() { Uid = userid }).ConfigureAwait(false);
await _authServiceClient.RemoveAuthAsync(new UidMessage() { Uid = userid }).ConfigureAwait(false);
_dbContext.RemoveRange(ownPairData);
await _dbContext.SaveChangesAsync().ConfigureAwait(false);

View File

@@ -5,7 +5,6 @@ using System.Threading.Tasks;
using MareSynchronos.API;
using MareSynchronosServer.Services;
using MareSynchronosServer.Utils;
using MareSynchronosShared.Authentication;
using MareSynchronosShared.Data;
using MareSynchronosShared.Metrics;
using MareSynchronosShared.Protos;
@@ -18,7 +17,6 @@ using Microsoft.Extensions.Logging;
namespace MareSynchronosServer.Hubs;
[Authorize]
public partial class MareHub : Hub<IMareHub>, IMareHub
{
private readonly MareMetrics _mareMetrics;

View File

@@ -1,8 +1,8 @@
using Grpc.Core;
using MareSynchronosShared.Metrics;
using MareSynchronosShared.Protos;
using MareSynchronosShared.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
@@ -12,7 +12,7 @@ using System.Threading.Tasks;
namespace MareSynchronosServer.Services;
public class GrpcClientIdentificationService : IHostedService
public class GrpcClientIdentificationService : GrpcBaseService
{
private readonly string _shardName;
private readonly ILogger<GrpcClientIdentificationService> _logger;
@@ -22,14 +22,11 @@ public class GrpcClientIdentificationService : IHostedService
private readonly MareMetrics _metrics;
protected readonly ConcurrentDictionary<string, UidWithIdent> OnlineClients = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, UidWithIdent> RemoteCachedIdents = new(StringComparer.Ordinal);
private bool _grpcIsFaulty = false;
private ConcurrentQueue<IdentChange> _identChangeQueue = new();
private CancellationTokenSource _streamCts = new();
private CancellationTokenSource _faultCheckCts = new();
public GrpcClientIdentificationService(ILogger<GrpcClientIdentificationService> logger, IdentificationService.IdentificationServiceClient gprcIdentClient,
IdentificationService.IdentificationServiceClient gprcIdentClientStreamOut,
IdentificationService.IdentificationServiceClient gprcIdentClientStreamIn, MareMetrics metrics, IConfiguration configuration)
IdentificationService.IdentificationServiceClient gprcIdentClientStreamIn, MareMetrics metrics, IConfiguration configuration) : base(logger)
{
var config = configuration.GetSection("MareSynchronos");
_shardName = config.GetValue("ShardName", "Main");
@@ -135,62 +132,6 @@ public class GrpcClientIdentificationService : IHostedService
});
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_ = RestartStreams();
_ = CheckGrpcFaults(_faultCheckCts.Token);
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_faultCheckCts.Cancel();
_streamCts.Cancel();
await ExecuteOnGrpc(_grpcIdentClient.ClearIdentsForServerAsync(new ServerMessage() { ServerId = _shardName })).ConfigureAwait(false);
}
private async Task CheckGrpcFaults(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
try
{
await CheckFaultStateAndResend().ConfigureAwait(false);
}
catch { SetGrpcFaulty(); }
await Task.Delay(250).ConfigureAwait(false);
}
}
private async Task RestartStreams()
{
_streamCts?.Cancel();
_streamCts?.Dispose();
_streamCts = new();
if (!_grpcIsFaulty)
{
try
{
await _grpcIdentClient.ClearIdentsForServerAsync(new ServerMessage() { ServerId = _shardName }).ConfigureAwait(false);
RemoteCachedIdents.Clear();
_ = StreamOnlineClientData(_streamCts.Token);
_ = ReceiveOnlineClientData(_streamCts.Token);
var remoteOnlineClients = await _grpcIdentClient.GetAllIdentsAsync(new ServerMessage()
{
ServerId = _shardName
}).ConfigureAwait(false);
foreach (var result in remoteOnlineClients.UidWithIdent)
{
RemoteCachedIdents[result.Uid.Uid] = result;
}
}
catch
{
SetGrpcFaulty();
}
}
}
private async Task StreamOnlineClientData(CancellationToken cts)
{
try
@@ -254,59 +195,49 @@ public class GrpcClientIdentificationService : IHostedService
}
}
private async Task<T> InvokeOnGrpc<T>(AsyncUnaryCall<T> toExecute)
protected override Task StartAsyncInternal(CancellationToken cancellationToken)
{
try
{
var result = await toExecute.ConfigureAwait(false);
await CheckFaultStateAndResend().ConfigureAwait(false);
return result;
}
catch
{
SetGrpcFaulty();
return default;
}
return Task.CompletedTask;
}
private async Task ExecuteOnGrpc<T>(AsyncUnaryCall<T> toExecute)
protected override async Task StopAsyncInternal(CancellationToken cancellationToken)
{
try
{
await toExecute.ConfigureAwait(false);
await CheckFaultStateAndResend().ConfigureAwait(false);
}
catch
{
SetGrpcFaulty();
}
await ExecuteOnGrpc(_grpcIdentClient.ClearIdentsForServerAsync(new ServerMessage() { ServerId = _shardName })).ConfigureAwait(false);
}
private async Task CheckFaultStateAndResend()
protected override async Task OnGrpcRestore()
{
if (_grpcIsFaulty)
var msg = new ServerIdentMessage();
msg.Idents.AddRange(OnlineClients.Select(c => new SetIdentMessage()
{
await RestartStreams().ConfigureAwait(false);
var msg = new ServerIdentMessage();
msg.Idents.AddRange(OnlineClients.Select(c => new SetIdentMessage()
{
UidWithIdent = c.Value
}));
await _grpcIdentClient.RecreateServerIdentsAsync(msg).ConfigureAwait(false);
_logger.LogInformation("GRPC connection is restored");
_grpcIsFaulty = false;
}
UidWithIdent = c.Value
}));
await _grpcIdentClient.RecreateServerIdentsAsync(msg).ConfigureAwait(false);
}
private void SetGrpcFaulty()
protected override async Task PreStartStream()
{
if (!_grpcIsFaulty)
await _grpcIdentClient.ClearIdentsForServerAsync(new ServerMessage() { ServerId = _shardName }).ConfigureAwait(false);
RemoteCachedIdents.Clear();
}
protected override Task StartStream(CancellationToken ct)
{
_ = StreamOnlineClientData(ct);
_ = ReceiveOnlineClientData(ct);
return Task.CompletedTask;
}
protected override async Task PostStartStream()
{
var remoteOnlineClients = await _grpcIdentClient.GetAllIdentsAsync(new ServerMessage()
{
_grpcIsFaulty = true;
_logger.LogWarning("GRPC connection is faulty");
ServerId = _shardName
}).ConfigureAwait(false);
foreach (var result in remoteOnlineClients.UidWithIdent)
{
RemoteCachedIdents[result.Uid.Uid] = result;
}
}
}

View File

@@ -23,6 +23,7 @@ using MareSynchronosServer.Services;
using System.Net.Http;
using MareSynchronosServer.Utils;
using MareSynchronosServer.RequirementHandlers;
using MareSynchronosShared.Services;
namespace MareSynchronosServer;
@@ -119,8 +120,10 @@ public class Startup
};
});
services.AddSingleton<GrpcAuthenticationService>();
services.AddSingleton<GrpcClientIdentificationService>();
services.AddTransient<IAuthorizationHandler, UserRequirementHandler>();
services.AddHostedService(p => p.GetService<GrpcAuthenticationService>());
services.AddHostedService(p => p.GetService<GrpcClientIdentificationService>());
services.AddDbContextPool<MareDbContext>(options =>

View File

@@ -4,7 +4,7 @@ using System.Threading.Tasks;
namespace MareSynchronosServices.Authentication;
public class FailedAuthorization : IDisposable
internal class FailedAuthorization : IDisposable
{
private int failedAttempts = 1;
public int FailedAttempts => failedAttempts;

View File

@@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging;
namespace MareSynchronosServices.Authentication;
public class SecretKeyAuthenticationHandler
internal class SecretKeyAuthenticationHandler
{
private readonly ILogger<SecretKeyAuthenticationHandler> logger;
private readonly MareMetrics metrics;
@@ -60,7 +60,7 @@ public class SecretKeyAuthenticationHandler
if (string.IsNullOrEmpty(secretKey))
{
metrics.IncCounter(MetricsAPI.CounterAuthenticationFailures);
return new AuthReply() { Success = false, Uid = string.Empty };
return new AuthReply() { Success = false, Uid = new UidMessage() { Uid = string.Empty } };
}
lock (failedAuthLock)
@@ -86,7 +86,7 @@ public class SecretKeyAuthenticationHandler
}, token);
logger.LogWarning("TempBan {ip} for authorization spam", ip);
return new AuthReply() { Success = false, Uid = string.Empty };
return new AuthReply() { Success = false, Uid = new UidMessage() { Uid = string.Empty } };
}
}
@@ -115,7 +115,7 @@ public class SecretKeyAuthenticationHandler
}
}
return new AuthReply() { Success = false, Uid = string.Empty };
return new AuthReply() { Success = false, Uid = new UidMessage() { Uid = string.Empty } };
}
metrics.IncCounter(MetricsAPI.CounterAuthenticationCacheHits);
@@ -152,7 +152,7 @@ public class SecretKeyAuthenticationHandler
}
metrics.IncCounter(MetricsAPI.CounterAuthenticationFailures);
return new AuthReply() { Success = false, Uid = string.Empty };
return new AuthReply() { Success = false, Uid = new UidMessage() { Uid = string.Empty } };
}
lock (authDictLock)
@@ -163,7 +163,7 @@ public class SecretKeyAuthenticationHandler
metrics.IncCounter(MetricsAPI.CounterAuthenticationSuccesses);
return new AuthReply() { Success = true, Uid = uid };
return new AuthReply() { Success = true, Uid = new UidMessage() { Uid = uid } };
}
public SecretKeyAuthenticationHandler(IConfiguration configuration, ILogger<SecretKeyAuthenticationHandler> logger, MareMetrics metrics)

View File

@@ -16,7 +16,7 @@ using System.Threading.Tasks;
namespace MareSynchronosServices;
public class CleanupService : IHostedService, IDisposable
internal class CleanupService : IHostedService, IDisposable
{
private readonly MareMetrics metrics;
private readonly SecretKeyAuthenticationHandler _authService;

View File

@@ -1,6 +1,5 @@
using MareSynchronosShared.Protos;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@@ -101,10 +100,10 @@ internal class IdentityHandler
{
identChanges[serverId] = new ConcurrentQueue<IdentChange>();
}
}
internal record ServerIdentity
{
public string ServerId { get; set; } = string.Empty;
public string CharacterIdent { get; set; } = string.Empty;
internal record ServerIdentity
{
public string ServerId { get; set; } = string.Empty;
public string CharacterIdent { get; set; } = string.Empty;
}
}

View File

@@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace MareSynchronosServices.Services;
public class AuthenticationService : AuthService.AuthServiceBase
internal class AuthenticationService : AuthService.AuthServiceBase
{
private readonly ILogger<AuthenticationService> _logger;
private readonly MareDbContext _dbContext;
@@ -20,12 +20,16 @@ public class AuthenticationService : AuthService.AuthServiceBase
_authHandler = authHandler;
}
public override async Task<AuthReply> Authorize(AuthRequest request, ServerCallContext context)
public override async Task Authorize(IAsyncStreamReader<AuthRequest> requestStream, IServerStreamWriter<AuthReply> responseStream, ServerCallContext context)
{
return await _authHandler.AuthenticateAsync(_dbContext, request.Ip, request.SecretKey);
await foreach (var input in requestStream.ReadAllAsync(context.CancellationToken).ConfigureAwait(false))
{
var response = await _authHandler.AuthenticateAsync(_dbContext, input.Ip, input.SecretKey).ConfigureAwait(false);
await responseStream.WriteAsync(response, context.CancellationToken).ConfigureAwait(false);
}
}
public override Task<Empty> RemoveAuth(RemoveAuthRequest request, ServerCallContext context)
public override Task<Empty> RemoveAuth(UidMessage request, ServerCallContext context)
{
_logger.LogInformation("Removing Authentication for {uid}", request.Uid);
_authHandler.RemoveAuthentication(request.Uid);

View File

@@ -1,7 +1,7 @@
using System.Security.Claims;
using System.Text.Encodings.Web;
using MareSynchronosServer;
using MareSynchronosShared.Protos;
using MareSynchronosShared.Services;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -14,13 +14,13 @@ public class SecretKeyGrpcAuthenticationHandler : AuthenticationHandler<Authenti
{
public const string AuthScheme = "SecretKeyGrpcAuth";
private readonly AuthService.AuthServiceClient _authClient;
private readonly GrpcAuthenticationService _grpcAuthService;
private readonly IHttpContextAccessor _accessor;
public SecretKeyGrpcAuthenticationHandler(IHttpContextAccessor accessor, AuthService.AuthServiceClient authClient,
public SecretKeyGrpcAuthenticationHandler(IHttpContextAccessor accessor, GrpcAuthenticationService authClient,
IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
this._authClient = authClient;
this._grpcAuthService = authClient;
_accessor = accessor;
}
@@ -33,7 +33,7 @@ public class SecretKeyGrpcAuthenticationHandler : AuthenticationHandler<Authenti
var ip = _accessor.GetIpAddress();
var authResult = await _authClient.AuthorizeAsync(new AuthRequest() { Ip = ip, SecretKey = authHeader }).ConfigureAwait(false);
var authResult = await _grpcAuthService.AuthorizeAsync(ip, authHeader).ConfigureAwait(false);
if (!authResult.Success)
{
@@ -44,7 +44,7 @@ public class SecretKeyGrpcAuthenticationHandler : AuthenticationHandler<Authenti
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, uid),
new(ClaimTypes.NameIdentifier, uid.Uid),
new(ClaimTypes.Authentication, authHeader)
};

View File

@@ -1,5 +1,4 @@
using MareSynchronosShared.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace MareSynchronosShared.Data;

View File

@@ -1,5 +1,4 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View File

@@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MareSynchronosShared.Models;
namespace MareSynchronosShared.Models;
public class GroupBan
{

View File

@@ -5,8 +5,8 @@ option csharp_namespace = "MareSynchronosShared.Protos";
package mareservices;
service AuthService {
rpc Authorize (AuthRequest) returns (AuthReply);
rpc RemoveAuth (RemoveAuthRequest) returns (Empty);
rpc Authorize (stream AuthRequest) returns (stream AuthReply);
rpc RemoveAuth (UidMessage) returns (Empty);
rpc ClearUnauthorized (Empty) returns (Empty);
}
@@ -101,10 +101,6 @@ message FileSizeResponse {
map<string, int64> hashToFileSize = 1;
}
message RemoveAuthRequest {
string uid = 1;
}
message AuthRequest {
string ip = 1;
string secretKey = 2;
@@ -112,5 +108,5 @@ message AuthRequest {
message AuthReply {
bool success = 1;
string uid = 2;
UidMessage uid = 2;
}

View File

@@ -0,0 +1,109 @@
using System.Collections.Concurrent;
using System.Security.Cryptography;
using MareSynchronosShared.Protos;
using Microsoft.Extensions.Logging;
namespace MareSynchronosShared.Services;
public class GrpcAuthenticationService : GrpcBaseService
{
private record AuthRequestInternal
{
public AuthRequest Request { get; set; }
public long Id { get; set; }
}
private readonly AuthService.AuthServiceClient _authClient;
private readonly ConcurrentQueue<AuthRequestInternal> _requestQueue = new();
private readonly ConcurrentDictionary<long, AuthReply> _authReplies = new();
private long _requestId = 0;
public GrpcAuthenticationService(ILogger<GrpcAuthenticationService> logger, AuthService.AuthServiceClient authClient) : base(logger)
{
_authClient = authClient;
}
public async Task<AuthReply> AuthorizeAsync(string ip, string secretKey)
{
using var sha1 = SHA1.Create();
var id = Interlocked.Increment(ref _requestId);
_requestQueue.Enqueue(new AuthRequestInternal()
{
Id = id,
Request = new AuthRequest()
{
Ip = ip,
SecretKey = secretKey,
}
});
using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30));
AuthReply response = null;
while (!GrpcIsFaulty && !cts.IsCancellationRequested && !_authReplies.TryRemove(id, out response))
{
await Task.Delay(10, cts.Token).ConfigureAwait(false);
}
return response ?? new AuthReply
{
Success = false,
};
}
public async Task GrpcAuthStream(CancellationToken token)
{
try
{
using var stream = _authClient.Authorize(cancellationToken: token);
while (!token.IsCancellationRequested)
{
while (_requestQueue.TryDequeue(out var request))
{
await stream.RequestStream.WriteAsync(request.Request, token).ConfigureAwait(false);
await stream.ResponseStream.MoveNext(token).ConfigureAwait(false);
_authReplies[request.Id] = stream.ResponseStream.Current;
}
await Task.Delay(10, token).ConfigureAwait(false);
}
}
catch
{
SetGrpcFaulty();
}
}
protected override Task OnGrpcRestore()
{
return Task.CompletedTask;
}
protected override Task PostStartStream()
{
return Task.CompletedTask;
}
protected override Task PreStartStream()
{
_requestQueue.Clear();
_authReplies.Clear();
return Task.CompletedTask;
}
protected override Task StartAsyncInternal(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
protected override Task StartStream(CancellationToken ct)
{
_ = GrpcAuthStream(ct);
return Task.CompletedTask;
}
protected override Task StopAsyncInternal(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,129 @@
using Grpc.Core;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace MareSynchronosShared.Services;
public abstract class GrpcBaseService : IHostedService, IDisposable
{
protected GrpcBaseService(ILogger logger)
{
_logger = logger;
}
private CancellationTokenSource _faultCheckCts = new();
private CancellationTokenSource _streamCts = new();
private readonly ILogger _logger;
protected bool GrpcIsFaulty { get; private set; }
protected abstract Task StartAsyncInternal(CancellationToken cancellationToken);
protected abstract Task StopAsyncInternal(CancellationToken cancellationToken);
protected abstract Task OnGrpcRestore();
protected abstract Task PreStartStream();
protected abstract Task StartStream(CancellationToken ct);
protected abstract Task PostStartStream();
public async Task StartAsync(CancellationToken cancellationToken)
{
_ = RestartStreams();
_ = CheckGrpcFaults(_faultCheckCts.Token);
await StartAsyncInternal(cancellationToken).ConfigureAwait(false);
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_faultCheckCts.Cancel();
_streamCts.Cancel();
await StopAsyncInternal(cancellationToken).ConfigureAwait(false);
}
private async Task RestartStreams()
{
_streamCts?.Cancel();
_streamCts?.Dispose();
_streamCts = new();
if (!GrpcIsFaulty)
{
try
{
await PreStartStream().ConfigureAwait(false);
await StartStream(_streamCts.Token).ConfigureAwait(false);
await PostStartStream().ConfigureAwait(false);
}
catch
{
SetGrpcFaulty();
}
}
}
protected void SetGrpcFaulty()
{
if (!GrpcIsFaulty)
{
GrpcIsFaulty = true;
_logger.LogWarning("GRPC connection is faulty");
}
}
private async Task CheckGrpcFaults(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
try
{
await CheckFaultStateAndResend().ConfigureAwait(false);
}
catch { SetGrpcFaulty(); }
await Task.Delay(250).ConfigureAwait(false);
}
}
private async Task CheckFaultStateAndResend()
{
if (GrpcIsFaulty)
{
await RestartStreams().ConfigureAwait(false);
await OnGrpcRestore().ConfigureAwait(false);
_logger.LogInformation("GRPC connection is restored");
GrpcIsFaulty = false;
}
}
protected async Task<T> InvokeOnGrpc<T>(AsyncUnaryCall<T> toExecute)
{
try
{
var result = await toExecute.ConfigureAwait(false);
await CheckFaultStateAndResend().ConfigureAwait(false);
return result;
}
catch
{
SetGrpcFaulty();
return default;
}
}
protected async Task ExecuteOnGrpc<T>(AsyncUnaryCall<T> toExecute)
{
try
{
await toExecute.ConfigureAwait(false);
await CheckFaultStateAndResend().ConfigureAwait(false);
}
catch
{
SetGrpcFaulty();
}
}
public void Dispose()
{
_streamCts?.Dispose();
_faultCheckCts?.Dispose();
}
}

View File

@@ -1,11 +1,6 @@
using MareSynchronosShared.Data;
using MareSynchronosShared.Models;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MareSynchronosShared.Utils;

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@@ -3,6 +3,7 @@ using MareSynchronosShared.Authentication;
using MareSynchronosShared.Data;
using MareSynchronosShared.Metrics;
using MareSynchronosShared.Protos;
using MareSynchronosShared.Services;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
@@ -47,6 +48,7 @@ public class Startup
}
};
services.AddSingleton<GrpcAuthenticationService>();
services.AddSingleton(new MareMetrics(new List<string> {
}, new List<string>
{
@@ -72,6 +74,7 @@ public class Startup
}, mareSettings.GetValue("DbContextPoolSize", 1024));
services.AddHostedService<CleanupService>();
services.AddHostedService(p => p.GetService<GrpcAuthenticationService>());
services.AddAuthentication(options =>
{