File: Storage\AbstractPersistentStorageService.cs
Web Access
Project: ..\..\..\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// 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;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Storage
{
    /// <summary>
    /// A service that enables storing and retrieving of information associated with solutions,
    /// projects or documents across runtime sessions.
    /// </summary>
    internal abstract partial class AbstractPersistentStorageService : IChecksummedPersistentStorageService
    {
        protected readonly IPersistentStorageConfiguration Configuration;
 
        /// <summary>
        /// This lock guards all mutable fields in this type.
        /// </summary>
        private readonly SemaphoreSlim _lock = new(initialCount: 1);
        private ReferenceCountedDisposable<IChecksummedPersistentStorage>? _currentPersistentStorage;
        private SolutionId? _currentPersistentStorageSolutionId;
 
        protected AbstractPersistentStorageService(IPersistentStorageConfiguration configuration)
            => Configuration = configuration;
 
        protected abstract string GetDatabaseFilePath(string workingFolderPath);
 
        /// <summary>
        /// Can throw.  If it does, the caller (<see cref="CreatePersistentStorageAsync"/>) will attempt
        /// to delete the database and retry opening one more time.  If that fails again, the <see
        /// cref="NoOpPersistentStorage"/> instance will be used.
        /// </summary>
        protected abstract ValueTask<IChecksummedPersistentStorage?> TryOpenDatabaseAsync(SolutionKey solutionKey, string workingFolderPath, string databaseFilePath, CancellationToken cancellationToken);
        protected abstract bool ShouldDeleteDatabase(Exception exception);
 
        public ValueTask<IChecksummedPersistentStorage> GetStorageAsync(SolutionKey solutionKey, CancellationToken cancellationToken)
        {
            return solutionKey.FilePath == null
                ? new(NoOpPersistentStorage.GetOrThrow(Configuration.ThrowOnFailure))
                : GetStorageWorkerAsync(solutionKey, cancellationToken);
        }
 
        internal async ValueTask<IChecksummedPersistentStorage> GetStorageWorkerAsync(SolutionKey solutionKey, CancellationToken cancellationToken)
        {
            using (await _lock.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
            {
                // Do we already have storage for this?
                if (solutionKey.Id == _currentPersistentStorageSolutionId)
                {
                    // We do, great. Increment our ref count for our caller.  They'll decrement it
                    // when done with it.
                    return PersistentStorageReferenceCountedDisposableWrapper.AddReferenceCountToAndCreateWrapper(_currentPersistentStorage!);
                }
 
                var workingFolder = Configuration.TryGetStorageLocation(solutionKey);
                if (workingFolder == null)
                    return NoOpPersistentStorage.GetOrThrow(Configuration.ThrowOnFailure);
 
                // If we already had some previous cached service, let's let it start cleaning up
                if (_currentPersistentStorage != null)
                {
                    var storageToDispose = _currentPersistentStorage;
 
                    // Kick off a task to actually go dispose the previous cached storage instance.
                    // This will remove the single ref count we ourselves added when we cached the
                    // instance.  Then once all other existing clients who are holding onto this
                    // instance let go, it will finally get truly disposed.
                    // This operation is not safe to cancel (as dispose must happen).
                    _ = Task.Run(storageToDispose.Dispose, CancellationToken.None);
 
                    _currentPersistentStorage = null;
                    _currentPersistentStorageSolutionId = null;
                }
 
                var storage = await CreatePersistentStorageAsync(solutionKey, workingFolder, cancellationToken).ConfigureAwait(false);
                Contract.ThrowIfNull(storage);
 
                // Create and cache a new storage instance associated with this particular solution.
                // It will initially have a ref-count of 1 due to our reference to it.
                _currentPersistentStorage = new ReferenceCountedDisposable<IChecksummedPersistentStorage>(storage);
                _currentPersistentStorageSolutionId = solutionKey.Id;
 
                // Now increment the reference count and return to our caller.  The current ref
                // count for this instance will be 2.  Until all the callers *and* us decrement
                // the refcounts, this instance will not be actually disposed.
                return PersistentStorageReferenceCountedDisposableWrapper.AddReferenceCountToAndCreateWrapper(_currentPersistentStorage);
            }
        }
 
        private async ValueTask<IChecksummedPersistentStorage> CreatePersistentStorageAsync(
            SolutionKey solutionKey, string workingFolderPath, CancellationToken cancellationToken)
        {
            // Attempt to create the database up to two times.  The first time we may encounter
            // some sort of issue (like DB corruption).  We'll then try to delete the DB and can
            // try to create it again.  If we can't create it the second time, then there's nothing
            // we can do and we have to store things in memory.
            var result = await TryCreatePersistentStorageAsync(solutionKey, workingFolderPath, cancellationToken).ConfigureAwait(false) ??
                         await TryCreatePersistentStorageAsync(solutionKey, workingFolderPath, cancellationToken).ConfigureAwait(false);
 
            if (result != null)
                return result;
 
            return NoOpPersistentStorage.GetOrThrow(Configuration.ThrowOnFailure);
        }
 
        private async ValueTask<IChecksummedPersistentStorage?> TryCreatePersistentStorageAsync(
            SolutionKey solutionKey,
            string workingFolderPath,
            CancellationToken cancellationToken)
        {
            var databaseFilePath = GetDatabaseFilePath(workingFolderPath);
            try
            {
                return await TryOpenDatabaseAsync(solutionKey, workingFolderPath, databaseFilePath, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception e) when (Recover(e))
            {
                return null;
            }
 
            bool Recover(Exception ex)
            {
                StorageDatabaseLogger.LogException(ex);
 
                if (Configuration.ThrowOnFailure)
                {
                    return false;
                }
 
                if (ShouldDeleteDatabase(ex))
                {
                    // this was not a normal exception that we expected during DB open.
                    // Report this so we can try to address whatever is causing this.
                    FatalError.ReportAndCatch(ex);
                    IOUtilities.PerformIO(() => Directory.Delete(Path.GetDirectoryName(databaseFilePath)!, recursive: true));
                }
 
                return true;
            }
        }
 
        private void Shutdown()
        {
            ReferenceCountedDisposable<IChecksummedPersistentStorage>? storage = null;
 
            lock (_lock)
            {
                // We will transfer ownership in a thread-safe way out so we can dispose outside the lock
                storage = _currentPersistentStorage;
                _currentPersistentStorage = null;
                _currentPersistentStorageSolutionId = null;
            }
 
            // Dispose storage outside of the lock. Note this only removes our reference count; clients who are still
            // using this will still be holding a reference count.
            storage?.Dispose();
        }
 
        internal TestAccessor GetTestAccessor()
            => new(this);
 
        internal readonly struct TestAccessor
        {
            private readonly AbstractPersistentStorageService _service;
 
            public TestAccessor(AbstractPersistentStorageService service)
                => _service = service;
 
            public void Shutdown()
                => _service.Shutdown();
        }
 
        /// <summary>
        /// A trivial wrapper that we can hand out for instances from the <see cref="AbstractPersistentStorageService"/>
        /// that wraps the underlying <see cref="IPersistentStorage"/> singleton.
        /// </summary>
        private sealed class PersistentStorageReferenceCountedDisposableWrapper : IChecksummedPersistentStorage
        {
            private readonly ReferenceCountedDisposable<IChecksummedPersistentStorage> _storage;
 
            private PersistentStorageReferenceCountedDisposableWrapper(ReferenceCountedDisposable<IChecksummedPersistentStorage> storage)
                => _storage = storage;
 
            public static IChecksummedPersistentStorage AddReferenceCountToAndCreateWrapper(ReferenceCountedDisposable<IChecksummedPersistentStorage> storage)
            {
                // This should only be called from a caller that has a non-null storage that it
                // already has a reference on.  So .TryAddReference cannot fail.
                return new PersistentStorageReferenceCountedDisposableWrapper(storage.TryAddReference() ?? throw ExceptionUtilities.Unreachable());
            }
 
            public void Dispose()
                => _storage.Dispose();
 
            public ValueTask DisposeAsync()
                => _storage.DisposeAsync();
 
            public Task<bool> ChecksumMatchesAsync(string name, Checksum checksum, CancellationToken cancellationToken)
                => _storage.Target.ChecksumMatchesAsync(name, checksum, cancellationToken);
 
            public Task<bool> ChecksumMatchesAsync(Project project, string name, Checksum checksum, CancellationToken cancellationToken)
                => _storage.Target.ChecksumMatchesAsync(project, name, checksum, cancellationToken);
 
            public Task<bool> ChecksumMatchesAsync(Document document, string name, Checksum checksum, CancellationToken cancellationToken)
                => _storage.Target.ChecksumMatchesAsync(document, name, checksum, cancellationToken);
 
            public Task<bool> ChecksumMatchesAsync(ProjectKey project, string name, Checksum checksum, CancellationToken cancellationToken)
                => _storage.Target.ChecksumMatchesAsync(project, name, checksum, cancellationToken);
 
            public Task<bool> ChecksumMatchesAsync(DocumentKey document, string name, Checksum checksum, CancellationToken cancellationToken)
                => _storage.Target.ChecksumMatchesAsync(document, name, checksum, cancellationToken);
 
            public Task<Stream?> ReadStreamAsync(string name, CancellationToken cancellationToken)
                => _storage.Target.ReadStreamAsync(name, cancellationToken);
 
            public Task<Stream?> ReadStreamAsync(Project project, string name, CancellationToken cancellationToken)
                => _storage.Target.ReadStreamAsync(project, name, cancellationToken);
 
            public Task<Stream?> ReadStreamAsync(Document document, string name, CancellationToken cancellationToken)
                => _storage.Target.ReadStreamAsync(document, name, cancellationToken);
 
            public Task<Stream> ReadStreamAsync(string name, Checksum checksum, CancellationToken cancellationToken)
                => _storage.Target.ReadStreamAsync(name, checksum, cancellationToken);
 
            public Task<Stream> ReadStreamAsync(Project project, string name, Checksum checksum, CancellationToken cancellationToken)
                => _storage.Target.ReadStreamAsync(project, name, checksum, cancellationToken);
 
            public Task<Stream> ReadStreamAsync(Document document, string name, Checksum checksum, CancellationToken cancellationToken)
                => _storage.Target.ReadStreamAsync(document, name, checksum, cancellationToken);
 
            public Task<Stream> ReadStreamAsync(ProjectKey project, string name, Checksum checksum, CancellationToken cancellationToken)
                => _storage.Target.ReadStreamAsync(project, name, checksum, cancellationToken);
 
            public Task<Stream> ReadStreamAsync(DocumentKey document, string name, Checksum checksum, CancellationToken cancellationToken)
                => _storage.Target.ReadStreamAsync(document, name, checksum, cancellationToken);
 
            public Task<bool> WriteStreamAsync(string name, Stream stream, CancellationToken cancellationToken)
                => _storage.Target.WriteStreamAsync(name, stream, cancellationToken);
 
            public Task<bool> WriteStreamAsync(Project project, string name, Stream stream, CancellationToken cancellationToken)
                => _storage.Target.WriteStreamAsync(project, name, stream, cancellationToken);
 
            public Task<bool> WriteStreamAsync(Document document, string name, Stream stream, CancellationToken cancellationToken)
                => _storage.Target.WriteStreamAsync(document, name, stream, cancellationToken);
 
            public Task<bool> WriteStreamAsync(string name, Stream stream, Checksum checksum, CancellationToken cancellationToken)
                => _storage.Target.WriteStreamAsync(name, stream, checksum, cancellationToken);
 
            public Task<bool> WriteStreamAsync(Project project, string name, Stream stream, Checksum checksum, CancellationToken cancellationToken)
                => _storage.Target.WriteStreamAsync(project, name, stream, checksum, cancellationToken);
 
            public Task<bool> WriteStreamAsync(Document document, string name, Stream stream, Checksum checksum, CancellationToken cancellationToken)
                => _storage.Target.WriteStreamAsync(document, name, stream, checksum, cancellationToken);
 
            public Task<bool> WriteStreamAsync(ProjectKey projectKey, string name, Stream stream, Checksum checksum, CancellationToken cancellationToken)
                => _storage.Target.WriteStreamAsync(projectKey, name, stream, checksum, cancellationToken);
 
            public Task<bool> WriteStreamAsync(DocumentKey documentKey, string name, Stream stream, Checksum checksum, CancellationToken cancellationToken)
                => _storage.Target.WriteStreamAsync(documentKey, name, stream, checksum, cancellationToken);
        }
    }
}