using Grpc.Core; using MxGateway.Contracts; using MxGateway.Contracts.Proto; using MxGateway.Server.Security.Authorization; using MxGateway.Server.Sessions; using MxGateway.Server.Workers; namespace MxGateway.Server.Grpc; public sealed class MxAccessGatewayService( ISessionManager sessionManager, IGatewayRequestIdentityAccessor identityAccessor, MxAccessGrpcRequestValidator requestValidator, MxAccessGrpcMapper mapper, IEventStreamService eventStreamService, ILogger logger) : MxAccessGateway.MxAccessGatewayBase { public override async Task OpenSession( OpenSessionRequest request, ServerCallContext context) { try { requestValidator.ValidateOpenSession(request); GatewaySession session = await sessionManager .OpenSessionAsync( SessionOpenRequest.FromContract(request), ResolveClientIdentity(), context.CancellationToken) .ConfigureAwait(false); OpenSessionReply reply = new() { SessionId = session.SessionId, BackendName = session.BackendName, WorkerProcessId = session.WorkerProcessId ?? 0, WorkerProtocolVersion = GatewayContractInfo.WorkerProtocolVersion, DefaultCommandTimeout = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(session.CommandTimeout), ProtocolStatus = MxAccessGrpcMapper.Ok(), }; reply.Capabilities.Add("unary-open-session"); reply.Capabilities.Add("unary-close-session"); reply.Capabilities.Add("unary-invoke"); reply.Capabilities.Add("server-stream-events"); return reply; } catch (Exception exception) when (exception is not RpcException) { throw MapException(exception); } } public override async Task CloseSession( CloseSessionRequest request, ServerCallContext context) { try { requestValidator.ValidateCloseSession(request); SessionCloseResult result = await sessionManager .CloseSessionAsync(request.SessionId, context.CancellationToken) .ConfigureAwait(false); return new CloseSessionReply { SessionId = result.SessionId, FinalState = result.FinalState, ProtocolStatus = MxAccessGrpcMapper.Ok(result.AlreadyClosed ? "Session was already closed." : "Session closed."), }; } catch (Exception exception) when (exception is not RpcException) { throw MapException(exception); } } public override async Task Invoke( MxCommandRequest request, ServerCallContext context) { try { requestValidator.ValidateInvoke(request); WorkerCommand workerCommand = mapper.MapCommand(request); WorkerCommandReply workerReply = await sessionManager .InvokeAsync(request.SessionId, workerCommand, context.CancellationToken) .ConfigureAwait(false); return mapper.MapCommandReply(workerReply); } catch (Exception exception) when (exception is not RpcException) { throw MapException(exception); } } public override async Task StreamEvents( StreamEventsRequest request, IServerStreamWriter responseStream, ServerCallContext context) { try { requestValidator.ValidateStreamEvents(request); await foreach (MxEvent publicEvent in eventStreamService .StreamEventsAsync(request, context.CancellationToken) .WithCancellation(context.CancellationToken) .ConfigureAwait(false)) { await responseStream.WriteAsync(publicEvent).ConfigureAwait(false); } } catch (Exception exception) when (exception is not RpcException) { throw MapException(exception); } } private string? ResolveClientIdentity() { return identityAccessor.Current?.DisplayName ?? identityAccessor.Current?.KeyId; } private RpcException MapException(Exception exception) { if (exception is OperationCanceledException) { return new RpcException(new Status(StatusCode.Cancelled, "gRPC request was canceled.")); } if (exception is SessionManagerException sessionException) { return MapSessionException(sessionException); } if (exception is WorkerClientException workerClientException) { return MapWorkerClientException(workerClientException); } logger.LogWarning(exception, "Public gRPC request failed."); return new RpcException(new Status(StatusCode.Unavailable, "Gateway request failed before an MXAccess reply was available.")); } private static RpcException MapSessionException(SessionManagerException exception) { StatusCode statusCode = exception.ErrorCode switch { SessionManagerErrorCode.SessionNotFound => StatusCode.NotFound, SessionManagerErrorCode.SessionNotReady => StatusCode.FailedPrecondition, SessionManagerErrorCode.EventSubscriberAlreadyActive => StatusCode.ResourceExhausted, SessionManagerErrorCode.EventQueueOverflow => StatusCode.ResourceExhausted, SessionManagerErrorCode.SessionLimitExceeded => StatusCode.ResourceExhausted, SessionManagerErrorCode.OpenFailed => StatusCode.Unavailable, SessionManagerErrorCode.CloseFailed => StatusCode.Unavailable, _ => StatusCode.Unavailable, }; return new RpcException(new Status(statusCode, exception.Message)); } private static RpcException MapWorkerClientException(WorkerClientException exception) { StatusCode statusCode = exception.ErrorCode switch { WorkerClientErrorCode.CommandTimeout => StatusCode.DeadlineExceeded, WorkerClientErrorCode.GatewayShutdown => StatusCode.Cancelled, WorkerClientErrorCode.InvalidState => StatusCode.FailedPrecondition, WorkerClientErrorCode.ProtocolViolation => StatusCode.Internal, _ => StatusCode.Unavailable, }; return new RpcException(new Status(statusCode, exception.Message)); } }