using Microsoft.Extensions.Logging; using System; using System.Diagnostics; using System.Threading.Tasks; namespace Capnp.Rpc { class RemoteAnswerCapability : RemoteResolvingCapability { // Set DebugEmbargos to true to get logging output for calls. RPC calls are expected to // be on the critical path, hence very relevant for performance. We just can't afford // additional stuff on this path. Even if the logger filters the outputs away, there is // overhead for creating the Logger object, calling the Logger methods and deciding to // filter the output. This justifies the precompiler switch. #if DebugEmbargos ILogger Logger { get; } = Logging.CreateLogger(); #endif readonly PendingQuestion _question; readonly MemberAccessPath _access; Proxy _resolvedCap; public RemoteAnswerCapability(PendingQuestion question, MemberAccessPath access): base(question.RpcEndpoint) { _question = question ?? throw new ArgumentNullException(nameof(question)); _access = access ?? throw new ArgumentNullException(nameof(access)); _ = AwaitWhenResolved(); } async void ReAllowFinishWhenDone(Task task) { try { ++_pendingCallsOnPromise; await task; } catch { } finally { lock (_question.ReentrancyBlocker) { --_pendingCallsOnPromise; _question.AllowFinish(); } } } protected override void Dispose(bool disposing) { base.Dispose(disposing); lock (_question.ReentrancyBlocker) { _resolvedCap?.Dispose(); } } protected override Proxy ResolvedCap { get { lock (_question.ReentrancyBlocker) { if (_resolvedCap == null && !_question.IsTailCall && _question.IsReturned) { DeserializerState result; try { result = _question.WhenReturned.Result; } catch (AggregateException exception) { throw exception.InnerException; } _resolvedCap = new Proxy(_access.Eval(result)); } return _resolvedCap; } } } async Task AwaitWhenResolved() { await _question.WhenReturned; return ResolvedCap; } public override Task WhenResolved => AwaitWhenResolved(); protected override void GetMessageTarget(MessageTarget.WRITER wr) { wr.which = MessageTarget.WHICH.PromisedAnswer; wr.PromisedAnswer.QuestionId = _question.QuestionId; _access.Serialize(wr.PromisedAnswer); } internal override IPromisedAnswer DoCall(ulong interfaceId, ushort methodId, DynamicSerializerState args) { lock (_question.ReentrancyBlocker) { if (_question.StateFlags.HasFlag(PendingQuestion.State.Returned) && !_question.StateFlags.HasFlag(PendingQuestion.State.TailCall)) { if (ResolvedCap == null) { throw new RpcException("Answer did not resolve to expected capability"); } return CallOnResolution(interfaceId, methodId, args); } else { #if DebugEmbargos Logger.LogDebug("Call by proxy"); #endif if (_question.StateFlags.HasFlag(PendingQuestion.State.Disposed)) { throw new ObjectDisposedException(nameof(PendingQuestion)); } if (_question.StateFlags.HasFlag(PendingQuestion.State.FinishRequested)) { throw new InvalidOperationException("Finish request was already sent"); } _question.DisallowFinish(); ++_pendingCallsOnPromise; var promisedAnswer = base.DoCall(interfaceId, methodId, args); ReAllowFinishWhenDone(promisedAnswer.WhenReturned); async void DecrementPendingCallsOnPromiseWhenReturned() { try { await promisedAnswer.WhenReturned; } catch { } finally { lock (_question.ReentrancyBlocker) { --_pendingCallsOnPromise; } } } DecrementPendingCallsOnPromiseWhenReturned(); return promisedAnswer; } } } protected override Call.WRITER SetupMessage(DynamicSerializerState args, ulong interfaceId, ushort methodId) { var call = base.SetupMessage(args, interfaceId, methodId); call.Target.which = MessageTarget.WHICH.PromisedAnswer; call.Target.PromisedAnswer.QuestionId = _question.QuestionId; _access.Serialize(call.Target.PromisedAnswer); return call; } internal override void Freeze(out IRpcEndpoint boundEndpoint) { lock (_question.ReentrancyBlocker) { if (_question.StateFlags.HasFlag(PendingQuestion.State.Returned) && _pendingCallsOnPromise == 0) { if (ResolvedCap == null) { throw new RpcException("Answer did not resolve to expected capability"); } ResolvedCap.Freeze(out boundEndpoint); } else { ++_pendingCallsOnPromise; _question.DisallowFinish(); boundEndpoint = _ep; } } } internal override void Unfreeze() { lock (_question.ReentrancyBlocker) { if (_pendingCallsOnPromise > 0) { --_pendingCallsOnPromise; _question.AllowFinish(); } else { ResolvedCap?.Unfreeze(); } } } internal override void Export(IRpcEndpoint endpoint, CapDescriptor.WRITER writer) { lock (_question.ReentrancyBlocker) { if (_question.StateFlags.HasFlag(PendingQuestion.State.Disposed)) throw new ObjectDisposedException(nameof(PendingQuestion)); if (_question.StateFlags.HasFlag(PendingQuestion.State.Returned)) { ResolvedCap.Export(endpoint, writer); } else { if (_question.StateFlags.HasFlag(PendingQuestion.State.FinishRequested)) throw new InvalidOperationException("Finish request was already sent"); if (endpoint == _ep) { writer.which = CapDescriptor.WHICH.ReceiverAnswer; _access.Serialize(writer.ReceiverAnswer); writer.ReceiverAnswer.QuestionId = _question.QuestionId; } else if (_question.IsTailCall) { // FIXME: Resource management! We should prevent finishing this // cap as long as it is exported. Unfortunately, we cannot determine // when it gets removed from the export table. var vine = Vine.Create(this); uint id = endpoint.AllocateExport(vine, out bool first); writer.which = CapDescriptor.WHICH.SenderHosted; writer.SenderHosted = id; } else { this.ExportAsSenderPromise(endpoint, writer); } } } } protected override void ReleaseRemotely() { this.DisposeWhenResolved(); } } }