File: Storage\AbstractPersistentStorageService.cs
Web Access
Project: src\roslyn\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(IPersistentStorageConfiguration configuration) : IChecksummedPersistentStorageService
{
    protected readonly IPersistentStorageConfiguration Configuration = configuration;

    private readonly SemaphoreSlim _lock = new(initialCount: 1);
    private IChecksummedPersistentStorage? _currentPersistentStorage;

    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, IPersistentStorageFaultInjector? faultInjector, CancellationToken cancellationToken);

    public ValueTask<IChecksummedPersistentStorage> GetStorageAsync(SolutionKey solutionKey, CancellationToken cancellationToken)
        => GetStorageAsync(solutionKey, this.Configuration, faultInjector: null, cancellationToken);

    public async ValueTask<IChecksummedPersistentStorage> GetStorageAsync(
        SolutionKey solutionKey,
        IPersistentStorageConfiguration configuration,
        IPersistentStorageFaultInjector? faultInjector,
        CancellationToken cancellationToken)
    {
        if (solutionKey.FilePath == null)
            return NoOpPersistentStorage.GetOrThrow(solutionKey, Configuration.ThrowOnFailure);

        // Without taking the lock, see if we can lookup a storage for this key.
        var existing = _currentPersistentStorage;
        if (existing?.SolutionKey == solutionKey)
            return existing;

        var workingFolder = configuration.TryGetStorageLocation(solutionKey);
        if (workingFolder == null)
            return NoOpPersistentStorage.GetOrThrow(solutionKey, Configuration.ThrowOnFailure);

        using (await _lock.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
        {
            // Recheck if we have a storage for this key after taking the lock.
            if (_currentPersistentStorage?.SolutionKey != solutionKey)
                _currentPersistentStorage = await CreatePersistentStorageAsync(solutionKey, workingFolder, faultInjector, cancellationToken).ConfigureAwait(false);

            return _currentPersistentStorage;
        }
    }

    private async ValueTask<IChecksummedPersistentStorage> CreatePersistentStorageAsync(
        SolutionKey solutionKey, string workingFolderPath, IPersistentStorageFaultInjector? faultInjector, 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, faultInjector, cancellationToken).ConfigureAwait(false) ??
                     await TryCreatePersistentStorageAsync(solutionKey, workingFolderPath, faultInjector, cancellationToken).ConfigureAwait(false);

        if (result != null)
            return result;

        return NoOpPersistentStorage.GetOrThrow(solutionKey, Configuration.ThrowOnFailure);
    }

    private async ValueTask<IChecksummedPersistentStorage?> TryCreatePersistentStorageAsync(
        SolutionKey solutionKey,
        string workingFolderPath,
        IPersistentStorageFaultInjector? faultInjector,
        CancellationToken cancellationToken)
    {
        var databaseFilePath = GetDatabaseFilePath(workingFolderPath);
        try
        {
            return await TryOpenDatabaseAsync(solutionKey, workingFolderPath, databaseFilePath, faultInjector, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception e) when (Recover(e))
        {
            return null;
        }

        bool Recover(Exception ex)
        {
            StorageDatabaseLogger.LogException(ex);

            if (Configuration.ThrowOnFailure)
                return false;

            // 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;
        }
    }
}