File: Rpc\NativeProviderLifecycle.cs
Web Access
Project: src\src\Aspire.Hosting.Native.Server\Aspire.Hosting.Native.Server.csproj (Aspire.Hosting.Native.Server)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Immutable;
using System.Text.Json.Nodes;
using Aspire.Hosting.Native.Api;
using Aspire.Hosting.Native.Runtime;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Hosting.Native.Rpc;
 
internal sealed partial class NativeProviderRegistry
{
    private PublishedResource TrackProviderObject(Registration registration, NativeRpcConnection owner, string id,
        DeclaredResource declaration, string typeId)
    {
        lock (_gate)
        {
            if (registration.Resources.TryGetValue(id, out var existing))
            {
                if (!ReferenceEquals(existing.Owner, owner) || !ReferenceEquals(existing.Declaration.Composition, declaration.Composition) ||
                    existing.Declaration.Handle != declaration.Handle || existing.TypeId != typeId || existing.IsRetired)
                {
                    throw RpcFault.InvalidHandle("An integration object cannot change its original declaration authority.");
                }
                return existing;
            }
            if (registration.Resources.Count >= 4096)
            {
                throw RpcFault.ProviderLimit("Provider object capacity was reached.");
            }
            var bindings = registration.LifecycleCapabilities.TryGetValue(typeId, out var hooks)
                ? hooks.Select(metadata => new ProviderLifecycleBinding(registration.Id, id, typeId,
                    metadata.LifecyclePhase!, metadata.Id)).ToImmutableArray()
                : ImmutableArray<ProviderLifecycleBinding>.Empty;
            declaration.Composition.Model.SetProviderLifecycle(declaration.Handle, bindings);
            var resource = new PublishedResource(this, registration, owner, declaration, id, typeId);
            declaration.Composition.TrackProviderResource(resource);
            registration.Resources.Add(id, resource);
 
            return resource;
        }
    }
 
    public async Task InvokeAsync(ProviderLifecycleBinding binding, ResourceLifecycleAuthority authority, CancellationToken cancellationToken)
    {
        await Task.Yield();
        Registration registration;
        PublishedResource resource;
        lock (_gate)
        {
            if (!_providersById.TryGetValue(binding.ProviderId, out registration!))
            {
                throw RpcFault.ProviderDisconnected("The lifecycle provider is unavailable.");
            }
            if (!registration.Resources.TryGetValue(binding.ObjectId, out resource!) || resource.IsRetired ||
                resource.TypeId != binding.TypeId || !registration.Capabilities.TryGetValue(binding.CapabilityId, out var metadata) ||
                metadata.LifecyclePhase != authority.Phase)
            {
                throw RpcFault.InvalidHandle("The lifecycle object or phase authority is unavailable.");
            }
        }
        EnsureAvailable(registration);
        var connection = registration.Connection;
        var peer = registration.Peer;
        var scope = connection.AddLifecycleScope(new ResourceLifecycleScope(authority));
        var invocation = Guid.NewGuid().ToString();
        var metadataProjection = registration.Capabilities[binding.CapabilityId];
        var receiver = metadataProjection.TargetParameterName!;
        var arguments = new JsonObject
        {
            [receiver] = new JsonObject { ["$handle"] = binding.ObjectId, ["$type"] = binding.TypeId },
            ["scope"] = scope
        };
        using var deadline = new CancellationTokenSource(options.ProviderInvocationTimeout, options.TimeProvider);
        using var cancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, deadline.Token, peer.Closed);
        try
        {
            var result = await peer.RequestAsync("handleExternalCapability",
                new JsonArray(JsonValue.Create(binding.CapabilityId), arguments, JsonValue.Create(invocation)), cancellation.Token).ConfigureAwait(false);
            NativeProviderValueProtocol.ValidatePrimitive(result, metadataProjection.ReturnType);
            logger.LogDebug("Native provider lifecycle {Phase} completed.", authority.Phase);
        }
        catch (Exception)
        {
            // Revocation precedes cancellation acknowledgement: an in-flight
            // callback cannot keep executing native work after its phase ends.
            authority.Revoke();
            if (!peer.Closed.IsCancellationRequested)
            {
                using var cancelDeadline = new CancellationTokenSource(options.ProviderInvocationTimeout, options.TimeProvider);
                try
                {
                    await peer.RequestAsync("cancelProviderInvocation", new JsonArray(JsonValue.Create(invocation)),
                        cancelDeadline.Token).ConfigureAwait(false);
                }
                catch
                {
                    Disconnect(connection);
                }
            }
            const string message = "The provider lifecycle phase did not complete; it will not be replayed.";
            throw cancellationToken.IsCancellationRequested ? RpcFault.ProviderLifecycleCancelled(message) :
                deadline.IsCancellationRequested ? RpcFault.ProviderLifecycleTimeout(message) : RpcFault.ProviderLifecycleFailed(message);
        }
        finally
        {
            authority.Revoke();
            try
            {
                await authority.DrainAsync().ConfigureAwait(false);
            }
            finally
            {
                connection.RemoveLifecycleScopes();
            }
        }
    }
 
    private async Task ReleaseObjectAsync(PublishedResource resource)
    {
        await Task.Yield();
        INativeRpcPeer? peer;
        NativeRpcConnection? connection;
        ProviderCallbackLease[] callbacks;
        lock (_gate)
        {
            resource.IsRetired = true;
            resource.Registration.Resources.Remove(resource.ObjectId);
            callbacks = resource.Callbacks.ToArray();
            resource.Callbacks.Clear();
            peer = resource.Registration.Disconnected ? null : resource.Registration.Peer;
            connection = resource.Registration.Disconnected ? null : resource.Registration.Connection;
        }
        foreach (var callback in callbacks)
        {
            callback.Dispose();
        }
        if (peer is null || peer.Closed.IsCancellationRequested)
        {
            return;
        }
        using var deadline = new CancellationTokenSource(options.ProviderInvocationTimeout, options.TimeProvider);
        try
        {
            var result = await peer.RequestAsync("releaseIntegrationResource",
                new JsonArray(JsonValue.Create(resource.ObjectId)), deadline.Token).ConfigureAwait(false);
            if (result is not JsonValue success || !success.TryGetValue<bool>(out var released) || !released)
            {
                throw RpcFault.ProviderDisposalFailed("The provider did not release its retired object.");
            }
        }
        catch
        {
            // A provider that cannot honor release loses its connection, so no
            // callable objects or phase authorities remain registered locally.
            if (connection is not null)
            {
                Disconnect(connection);
            }
            throw RpcFault.ProviderDisposalFailed("The provider object release did not complete.");
        }
    }
 
    internal sealed class PublishedResource(NativeProviderRegistry registry, Registration registration,
        NativeRpcConnection owner, DeclaredResource declaration, string objectId, string typeId) : IProviderResourceRetirement
    {
        private readonly Lock _retirementGate = new();
        private Task? _retirement;
        internal Registration Registration { get; } = registration;
        internal NativeRpcConnection Owner { get; } = owner;
        internal DeclaredResource Declaration { get; } = declaration;
        internal string ObjectId { get; } = objectId;
        internal string TypeId { get; } = typeId;
        internal bool IsRetired { get; set; }
        internal List<ProviderCallbackLease> Callbacks { get; } = [];
        public Task RetireAsync()
        {
            lock (_retirementGate)
            {
                return _retirement ??= registry.ReleaseObjectAsync(this);
            }
        }
    }
}