File: Services\KeepAlive\RemoteKeepAliveService.cs
Web Access
Project: src\src\Workspaces\Remote\ServiceHub\Microsoft.CodeAnalysis.Remote.ServiceHub.csproj (Microsoft.CodeAnalysis.Remote.ServiceHub)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Threading;
 
namespace Microsoft.CodeAnalysis.Remote;
 
internal sealed partial class RemoteKeepAliveService : BrokeredServiceBase, IRemoteKeepAliveService
{
    internal sealed class Factory : FactoryBase<IRemoteKeepAliveService>
    {
        protected override IRemoteKeepAliveService CreateService(in ServiceConstructionArguments arguments)
            => new RemoteKeepAliveService(arguments);
    }
 
    /// <summary>
    /// Mapping from sessionId to the completion source that will be used to signal when that session's solution
    /// snapshot is fully hydrated on the OOP (this) side.  The `bool` result of the TaskCompletionSource is unused, and
    /// is only there because TaskCompletionSource requires some type on netstandard2.0.
    /// </summary>
    private readonly ConcurrentDictionary<long, TaskCompletionSource<bool>> _sessionIdToCompletionSource = new();
 
    public RemoteKeepAliveService(in ServiceConstructionArguments arguments)
        : base(arguments)
    {
    }
 
    private TaskCompletionSource<bool> GetSessionCompletionSource(long sessionId)
        => _sessionIdToCompletionSource.GetOrAdd(sessionId, static _ => new());
 
    public async ValueTask KeepAliveAsync(
        Checksum solutionChecksum,
        long sessionId,
        CancellationToken cancellationToken)
    {
        // Ensure we have a completion source for this sessionId.  Note: we are potentially racing with
        // WaitForSessionIdAsync on the host side, so it's possible that it will beat us to creating the entry in this
        // dictionary.  That's fine though, as both calls will be guaranteed to get the same completionSource thanks to
        // GetOrAdd.
        var completionSource = GetSessionCompletionSource(sessionId);
        try
        {
            // First get the solution, ensuring that it is currently pinned.
            await RunServiceAsync(solutionChecksum, async solution =>
            {
                // Now that we have the solution, we can trigger the completion source to let the host know it can 
                // proceed with its work.
                completionSource.TrySetResult(true);
 
                // Wait for our caller to tell us to cancel.  That way we can release this solution and allow it
                // to be collected if not needed anymore.
                //
                // This was provided by stoub as an idiomatic way to wait indefinitely until a cancellation token triggers.
                await Task.Delay(Timeout.Infinite, cancellationToken).ConfigureAwait(false);
            }, cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            // Now that we've been cancelled, remove the completion source from our map.  Note: this cancellationToken
            // is the one owned by RemoteKeepAliveSession, and only triggered in its Dispose method.  We only get to
            // that point once the RemoteKeepAliveSession is fully created, returned back out to the caller, and then
            // eventually Disposed of.  That means that by the time we get here, WaitForSessionIdAsync already must have
            // returned, so there is no concern about racing at this point, and we are certain to clear out the value
            // and not leave around stale entries.
            Contract.ThrowIfFalse(_sessionIdToCompletionSource.TryRemove(sessionId, out var foundSource));
            Contract.ThrowIfFalse(foundSource == completionSource);
        }
    }
 
    public async ValueTask WaitForSessionIdAsync(long sessionId, CancellationToken cancellationToken)
    {
        // Ensure we have a completion source for this sessionId.  Note: we are potentially racing with KeepAliveAsync
        // on the host side, so it's possible that it will beat us to creating the entry in this dictionary.  That's
        // fine though, as both calls will be guaranteed to get the same completionSource thanks to GetOrAdd.
        var completionSource = GetSessionCompletionSource(sessionId);
 
        // Now, wait for KeepAliveAsync to finally signal us to proceed.  We also listen for cancellation so that the
        // host can bail out if needed, without waiting on the full solution (or project cone) to sync over.
        await completionSource.Task.WithCancellation(cancellationToken).ConfigureAwait(false);
    }
}