diff --git a/eng/Versions.props b/eng/Versions.props
index d7fba73fd3d35..04b7536fee788 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -62,7 +62,7 @@
$(MicrosoftBuildPackagesVersion)
5.7.0
- 16.9.63
+ 16.10.11-alpha
true
+ 9
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Tools/IdeCoreBenchmarks/NavigateToBenchmarks.cs b/src/Tools/IdeCoreBenchmarks/NavigateToBenchmarks.cs
index 34a6dc8f009dc..bb36ed4e25064 100644
--- a/src/Tools/IdeCoreBenchmarks/NavigateToBenchmarks.cs
+++ b/src/Tools/IdeCoreBenchmarks/NavigateToBenchmarks.cs
@@ -5,16 +5,21 @@
#nullable disable
using System;
+using System.Collections.Generic;
using System.Collections.Immutable;
+using System.Diagnostics;
using System.IO;
using System.Linq;
+using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using AnalyzerRunner;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnosers;
+using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Host;
+using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.MSBuild;
using Microsoft.CodeAnalysis.NavigateTo;
using Microsoft.CodeAnalysis.Storage;
@@ -24,62 +29,78 @@ namespace IdeCoreBenchmarks
[MemoryDiagnoser]
public class NavigateToBenchmarks
{
- private readonly string _solutionPath;
-
- private MSBuildWorkspace _workspace;
-
- public NavigateToBenchmarks()
- {
- var roslynRoot = Environment.GetEnvironmentVariable(Program.RoslynRootPathEnvVariableName);
- _solutionPath = Path.Combine(roslynRoot, @"C:\github\roslyn\Compilers.sln");
-
- if (!File.Exists(_solutionPath))
- throw new ArgumentException("Couldn't find Roslyn.sln");
-
- Console.Write("Found roslyn.sln");
- }
-
- [GlobalSetup]
- public void Setup()
- {
- _workspace = AnalyzerRunnerHelper.CreateWorkspace();
- if (_workspace == null)
- throw new ArgumentException("Couldn't create workspace");
-
- _workspace.TryApplyChanges(_workspace.CurrentSolution.WithOptions(_workspace.Options
- .WithChangedOption(StorageOptions.Database, StorageDatabase.SQLite)));
-
- Console.WriteLine("Opening roslyn");
- var start = DateTime.Now;
- _ = _workspace.OpenSolutionAsync(_solutionPath, progress: null, CancellationToken.None).Result;
- Console.WriteLine("Finished opening roslyn: " + (DateTime.Now - start));
-
- var storageService = _workspace.Services.GetService();
- if (storageService == null)
- throw new ArgumentException("Couldn't get storage service");
-
- // Force a storage instance to be created. This makes it simple to go examine it prior to any operations we
- // perform, including seeing how big the initial string table is.
- using var storage = storageService.GetStorageAsync(_workspace.CurrentSolution, CancellationToken.None).AsTask().GetAwaiter().GetResult();
- }
-
- [GlobalCleanup]
- public void Cleanup()
- {
- _workspace?.Dispose();
- _workspace = null;
- }
-
[Benchmark]
public async Task RunNavigateTo()
{
- var solution = _workspace.CurrentSolution;
- // Search each project with an independent threadpool task.
- var searchTasks = solution.Projects.Select(
- p => Task.Run(() => SearchAsync(p, priorityDocuments: ImmutableArray.Empty), CancellationToken.None)).ToArray();
-
- var result = await Task.WhenAll(searchTasks).ConfigureAwait(false);
- var sum = result.Sum();
+ try
+ {
+ // QueryVisualStudioInstances returns Visual Studio installations on .NET Framework, and .NET Core SDK
+ // installations on .NET Core. We use the one with the most recent version.
+ var msBuildInstance = MSBuildLocator.QueryVisualStudioInstances().OrderByDescending(x => x.Version).First();
+
+ MSBuildLocator.RegisterInstance(msBuildInstance);
+
+ var roslynRoot = Environment.GetEnvironmentVariable(Program.RoslynRootPathEnvVariableName);
+ var solutionPath = Path.Combine(roslynRoot, @"C:\github\roslyn\Roslyn.sln");
+
+ if (!File.Exists(solutionPath))
+ throw new ArgumentException("Couldn't find Roslyn.sln");
+
+ Console.Write("Found Roslyn.sln: " + Process.GetCurrentProcess().Id);
+
+ var assemblies = MSBuildMefHostServices.DefaultAssemblies
+ .Add(typeof(AnalyzerRunnerHelper).Assembly)
+ .Add(typeof(FindReferencesBenchmarks).Assembly);
+ var services = MefHostServices.Create(assemblies);
+
+ var workspace = MSBuildWorkspace.Create(new Dictionary
+ {
+ // Use the latest language version to force the full set of available analyzers to run on the project.
+ { "LangVersion", "9.0" },
+ }, services);
+
+ if (workspace == null)
+ throw new ArgumentException("Couldn't create workspace");
+
+ workspace.TryApplyChanges(workspace.CurrentSolution.WithOptions(workspace.Options
+ .WithChangedOption(StorageOptions.Database, StorageDatabase.SQLite)
+ .WithChangedOption(StorageOptions.DatabaseMustSucceed, true)));
+
+ Console.WriteLine("Opening roslyn. Attach to: " + Process.GetCurrentProcess().Id);
+
+ var start = DateTime.Now;
+ var solution = workspace.OpenSolutionAsync(solutionPath, progress: null, CancellationToken.None).Result;
+ Console.WriteLine("Finished opening roslyn: " + (DateTime.Now - start));
+
+ // Force a storage instance to be created. This makes it simple to go examine it prior to any operations we
+ // perform, including seeing how big the initial string table is.
+ var storageService = workspace.Services.GetService();
+ if (storageService == null)
+ throw new ArgumentException("Couldn't get storage service");
+
+ using (var storage = await storageService.GetStorageAsync(workspace.CurrentSolution, CancellationToken.None))
+ {
+ Console.WriteLine();
+ }
+
+ Console.WriteLine("Starting navigate to");
+
+ start = DateTime.Now;
+ // Search each project with an independent threadpool task.
+ var searchTasks = solution.Projects.Select(
+ p => Task.Run(() => SearchAsync(p, priorityDocuments: ImmutableArray.Empty), CancellationToken.None)).ToArray();
+
+ var result = await Task.WhenAll(searchTasks).ConfigureAwait(false);
+ var sum = result.Sum();
+
+ start = DateTime.Now;
+ Console.WriteLine("Num results: " + (DateTime.Now - start));
+ }
+ catch (ReflectionTypeLoadException ex)
+ {
+ foreach (var ex2 in ex.LoaderExceptions)
+ Console.WriteLine(ex2);
+ }
}
private async Task SearchAsync(Project project, ImmutableArray priorityDocuments)
diff --git a/src/VisualStudio/CSharp/Test/Microsoft.VisualStudio.LanguageServices.CSharp.UnitTests.csproj b/src/VisualStudio/CSharp/Test/Microsoft.VisualStudio.LanguageServices.CSharp.UnitTests.csproj
index fcdff9c634dcb..3f6bde5d966f7 100644
--- a/src/VisualStudio/CSharp/Test/Microsoft.VisualStudio.LanguageServices.CSharp.UnitTests.csproj
+++ b/src/VisualStudio/CSharp/Test/Microsoft.VisualStudio.LanguageServices.CSharp.UnitTests.csproj
@@ -52,6 +52,7 @@
+
diff --git a/src/VisualStudio/CSharp/Test/PersistentStorage/AbstractPersistentStorageTests.cs b/src/VisualStudio/CSharp/Test/PersistentStorage/AbstractPersistentStorageTests.cs
index 4c54f7cca1358..d51cb83ca8fd7 100644
--- a/src/VisualStudio/CSharp/Test/PersistentStorage/AbstractPersistentStorageTests.cs
+++ b/src/VisualStudio/CSharp/Test/PersistentStorage/AbstractPersistentStorageTests.cs
@@ -12,9 +12,11 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
+using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PersistentStorage;
using Microsoft.CodeAnalysis.Storage;
using Microsoft.CodeAnalysis.Test.Utilities;
+using Microsoft.VisualStudio.LanguageServices.UnitTests;
using Roslyn.Test.Utilities;
using Xunit;
@@ -74,6 +76,9 @@ protected AbstractPersistentStorageTests()
ThreadPool.SetMinThreads(Math.Max(workerThreads, NumThreads), completionPortThreads);
}
+ internal abstract AbstractPersistentStorageService GetStorageService(
+ OptionSet options, IMefHostExportProvider exportProvider, IPersistentStorageLocationService locationService, IPersistentStorageFaultInjector? faultInjector, string rootFolder);
+
public void Dispose()
{
// This should cause the service to release the cached connection it maintains for the primary workspace
@@ -248,24 +253,6 @@ public async Task PersistentService_Document_SimultaneousWrites()
Assert.True(value < NumThreads);
}
- private void DoSimultaneousWrites(Func write)
- {
- var barrier = new Barrier(NumThreads);
- var countdown = new CountdownEvent(NumThreads);
- for (var i = 0; i < NumThreads; i++)
- {
- ThreadPool.QueueUserWorkItem(s =>
- {
- var id = (int)s;
- barrier.SignalAndWait();
- write(id + "").Wait();
- countdown.Signal();
- }, i);
- }
-
- countdown.Wait();
- }
-
[Theory]
[CombinatorialData]
public async Task PersistentService_Solution_SimultaneousReads(Size size, bool withChecksum)
@@ -843,13 +830,45 @@ private void DoSimultaneousReads(Func> read, string expectedValue)
Assert.Equal(new List(), exceptions);
}
+ private void DoSimultaneousWrites(Func write)
+ {
+ var barrier = new Barrier(NumThreads);
+ var countdown = new CountdownEvent(NumThreads);
+
+ var exceptions = new List();
+ for (var i = 0; i < NumThreads; i++)
+ {
+ ThreadPool.QueueUserWorkItem(s =>
+ {
+ var id = (int)s;
+ barrier.SignalAndWait();
+ try
+ {
+ write(id + "").Wait();
+ }
+ catch (Exception ex)
+ {
+ lock (exceptions)
+ {
+ exceptions.Add(ex);
+ }
+ }
+ countdown.Signal();
+ }, i);
+ }
+
+ countdown.Wait();
+
+ Assert.Empty(exceptions);
+ }
+
protected Solution CreateOrOpenSolution(bool nullPaths = false)
{
var solutionFile = _persistentFolder.CreateOrOpenFile("Solution1.sln").WriteAllText("");
var info = SolutionInfo.Create(SolutionId.CreateNewId(), VersionStamp.Create(), solutionFile.Path);
- var workspace = new AdhocWorkspace();
+ var workspace = new AdhocWorkspace(VisualStudioTestCompositions.LanguageServices.GetHostServices());
workspace.AddSolution(info);
var solution = workspace.CurrentSolution;
@@ -876,13 +895,15 @@ internal async Task GetStorageAsync(
_storageService?.GetTestAccessor().Shutdown();
var locationService = new MockPersistentStorageLocationService(solution.Id, _persistentFolder.Path);
- _storageService = GetStorageService((IMefHostExportProvider)solution.Workspace.Services.HostServices, locationService, faultInjector);
+ _storageService = GetStorageService(
+ solution.Options, (IMefHostExportProvider)solution.Workspace.Services.HostServices,
+ locationService, faultInjector, _persistentFolder.Path);
var storage = await _storageService.GetStorageAsync(solution, checkBranchId: true, CancellationToken.None);
// If we're injecting faults, we expect things to be strange
if (faultInjector == null)
{
- Assert.NotEqual(NoOpPersistentStorage.Instance, storage);
+ Assert.NotEqual(NoOpPersistentStorage.TestAccessor.StorageInstance, storage);
}
return storage;
@@ -895,13 +916,14 @@ internal async Task GetStorageFromKeyAsync(
_storageService?.GetTestAccessor().Shutdown();
var locationService = new MockPersistentStorageLocationService(solutionKey.Id, _persistentFolder.Path);
- _storageService = GetStorageService((IMefHostExportProvider)workspace.Services.HostServices, locationService, faultInjector);
+ _storageService = GetStorageService(
+ workspace.Options, (IMefHostExportProvider)workspace.Services.HostServices, locationService, faultInjector, _persistentFolder.Path);
var storage = await _storageService.GetStorageAsync(workspace, solutionKey, checkBranchId: true, CancellationToken.None);
// If we're injecting faults, we expect things to be strange
if (faultInjector == null)
{
- Assert.NotEqual(NoOpPersistentStorage.Instance, storage);
+ Assert.NotEqual(NoOpPersistentStorage.TestAccessor.StorageInstance, storage);
}
return storage;
@@ -927,8 +949,6 @@ public MockPersistentStorageLocationService(SolutionId solutionId, string storag
=> solutionKey.Id == _solutionId ? _storageLocation : null;
}
- internal abstract AbstractPersistentStorageService GetStorageService(IMefHostExportProvider exportProvider, IPersistentStorageLocationService locationService, IPersistentStorageFaultInjector? faultInjector);
-
protected Stream EncodeString(string text)
{
var bytes = _encoding.GetBytes(text);
@@ -940,14 +960,9 @@ private string ReadStringToEnd(Stream stream)
{
using (stream)
{
- var bytes = new byte[stream.Length];
- var count = 0;
- while (count < stream.Length)
- {
- count = stream.Read(bytes, count, (int)stream.Length - count);
- }
-
- return _encoding.GetString(bytes);
+ using var memoryStream = new MemoryStream();
+ stream.CopyTo(memoryStream);
+ return _encoding.GetString(memoryStream.ToArray());
}
}
}
diff --git a/src/VisualStudio/CSharp/Test/PersistentStorage/CloudCachePersistentStorageTests.cs b/src/VisualStudio/CSharp/Test/PersistentStorage/CloudCachePersistentStorageTests.cs
new file mode 100644
index 0000000000000..feb9fbd8b2fdd
--- /dev/null
+++ b/src/VisualStudio/CSharp/Test/PersistentStorage/CloudCachePersistentStorageTests.cs
@@ -0,0 +1,39 @@
+// 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.Linq;
+using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
+using Microsoft.CodeAnalysis.Host;
+using Microsoft.CodeAnalysis.Host.Mef;
+using Microsoft.CodeAnalysis.Options;
+using Microsoft.CodeAnalysis.Storage;
+using Microsoft.CodeAnalysis.UnitTests.WorkspaceServices.Mocks;
+
+namespace Microsoft.CodeAnalysis.UnitTests.WorkspaceServices
+{
+ public class CloudCachePersistentStorageTests : AbstractPersistentStorageTests
+ {
+ internal override AbstractPersistentStorageService GetStorageService(
+ OptionSet options, IMefHostExportProvider exportProvider, IPersistentStorageLocationService locationService, IPersistentStorageFaultInjector? faultInjector, string relativePathBase)
+ {
+ var threadingContext = exportProvider.GetExports().Single().Value;
+ return new MockCloudCachePersistentStorageService(
+ locationService,
+ relativePathBase,
+ cs =>
+ {
+ if (cs is IAsyncDisposable asyncDisposable)
+ {
+ threadingContext.JoinableTaskFactory.Run(
+ () => asyncDisposable.DisposeAsync().AsTask());
+ }
+ else if (cs is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ });
+ }
+ }
+}
diff --git a/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/AuthorizationServiceMock.cs b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/AuthorizationServiceMock.cs
new file mode 100644
index 0000000000000..405d6af003f63
--- /dev/null
+++ b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/AuthorizationServiceMock.cs
@@ -0,0 +1,36 @@
+// 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.
+
+// Copy of https://devdiv.visualstudio.com/DevDiv/_git/VS.CloudCache?path=%2Ftest%2FMicrosoft.VisualStudio.Cache.Tests%2FMocks&_a=contents&version=GBmain
+// Try to keep in sync and avoid unnecessary changes here.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.ServiceHub.Framework.Services;
+
+#pragma warning disable CS0067 // events that are never used
+
+namespace Microsoft.CodeAnalysis.UnitTests.WorkspaceServices.Mocks
+{
+ internal class AuthorizationServiceMock : IAuthorizationService
+ {
+ public event EventHandler? CredentialsChanged;
+
+ public event EventHandler? AuthorizationChanged;
+
+ internal bool Allow { get; set; } = true;
+
+ public ValueTask CheckAuthorizationAsync(ProtectedOperation operation, CancellationToken cancellationToken = default)
+ {
+ return new ValueTask(this.Allow);
+ }
+
+ public ValueTask> GetCredentialsAsync(CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/FileSystemServiceMock.cs b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/FileSystemServiceMock.cs
new file mode 100644
index 0000000000000..1e1481d0aef5d
--- /dev/null
+++ b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/FileSystemServiceMock.cs
@@ -0,0 +1,89 @@
+// 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.
+
+// Copy of https://devdiv.visualstudio.com/DevDiv/_git/VS.CloudCache?path=%2Ftest%2FMicrosoft.VisualStudio.Cache.Tests%2FMocks&_a=contents&version=GBmain
+// Try to keep in sync and avoid unnecessary changes here.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Pipelines;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.ServiceHub.Framework;
+using Microsoft.VisualStudio.RpcContracts.FileSystem;
+
+namespace Microsoft.CodeAnalysis.UnitTests.WorkspaceServices.Mocks
+{
+ internal class FileSystemServiceMock : IFileSystem
+ {
+ public event EventHandler? DirectoryEntryChanged;
+
+ public event EventHandler? RootEntriesChanged;
+
+ public Task ConvertLocalFileNameToRemoteUriAsync(string fileName, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task ConvertLocalFileNameToRemoteUriAsync(string fileName, string remoteScheme, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task ConvertLocalUriToRemoteUriAsync(Uri localUri, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task ConvertLocalUriToRemoteUriAsync(Uri localUri, string remoteScheme, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task ConvertRemoteFileNameToRemoteUriAsync(string fileName, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task ConvertRemoteFileNameToRemoteUriAsync(string fileName, string remoteScheme, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task ConvertRemoteUriToLocalUriAsync(Uri remoteUri, CancellationToken cancellationToken) => Task.FromResult(remoteUri);
+
+ public Task ConvertRemoteUriToRemoteFileNameAsync(Uri remoteUri, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task CopyAsync(Uri sourceUri, Uri destinationUri, bool overwrite, IProgress? progress, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task CreateDirectoryAsync(Uri uri, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task DeleteAsync(Uri uri, bool recursive, IProgress? progress, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task DownloadFileAsync(Uri remoteUri, IProgress? progress, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public IAsyncEnumerable EnumerateDirectoriesAsync(Uri uri, string searchPattern, SearchOption searchOption, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public IAsyncEnumerable EnumerateDirectoryEntriesAsync(Uri uri, string searchPattern, SearchOption searchOption, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public IAsyncEnumerable EnumerateFilesAsync(Uri uri, string searchPattern, SearchOption searchOption, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task GetDefaultRemoteUriSchemeAsync(CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task GetDisplayInfoAsync(Uri uri, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task GetDisplayInfoAsync(string fileName, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task GetInfoAsync(Uri uri, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task GetMonikerForFileSystemProviderAsync(string scheme, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task GetMonikerForRemoteFileSystemProviderAsync(string scheme, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task> GetRootEntriesAsync(string scheme, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task> GetRootEntriesAsync(CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task> GetSupportedSchemesAsync(CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task MoveAsync(Uri oldUri, Uri newUri, bool overwrite, IProgress? progress, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task ReadFileAsync(Uri uri, PipeWriter writer, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public ValueTask UnwatchAsync(WatchResult watchResult, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public ValueTask WatchDirectoryAsync(Uri uri, bool recursive, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public ValueTask WatchFileAsync(Uri uri, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task WriteFileAsync(Uri uri, PipeReader reader, bool overwrite, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ protected virtual void OnDirectoryEntryChanged(DirectoryEntryChangedEventArgs args) => this.DirectoryEntryChanged?.Invoke(this, args);
+
+ protected virtual void OnRootEntriesChanged(RootEntriesChangedEventArgs args) => this.RootEntriesChanged?.Invoke(this, args);
+ }
+}
diff --git a/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/MockCloudCachePersistentStorageService.cs b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/MockCloudCachePersistentStorageService.cs
new file mode 100644
index 0000000000000..894eb5b9f9f26
--- /dev/null
+++ b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/MockCloudCachePersistentStorageService.cs
@@ -0,0 +1,61 @@
+// 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.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Host;
+using Microsoft.ServiceHub.Framework;
+using Microsoft.ServiceHub.Framework.Services;
+using Microsoft.VisualStudio;
+using Microsoft.VisualStudio.Cache;
+using Microsoft.VisualStudio.Cache.SQLite;
+using Microsoft.VisualStudio.LanguageServices.Storage;
+using Microsoft.VisualStudio.RpcContracts.Caching;
+
+namespace Microsoft.CodeAnalysis.UnitTests.WorkspaceServices.Mocks
+{
+ internal class MockCloudCachePersistentStorageService : AbstractCloudCachePersistentStorageService
+ {
+ private readonly string _relativePathBase;
+ private readonly Action _disposeCacheService;
+
+ public MockCloudCachePersistentStorageService(
+ IPersistentStorageLocationService locationService,
+ string relativePathBase,
+ Action disposeCacheService)
+ : base(locationService)
+ {
+ _relativePathBase = relativePathBase;
+ _disposeCacheService = disposeCacheService;
+ }
+
+ protected override void DisposeCacheService(ICacheService cacheService)
+ => _disposeCacheService(cacheService);
+
+ protected override async ValueTask CreateCacheServiceAsync(CancellationToken cancellationToken)
+ {
+ // Directly access VS' CacheService through their library and not as a brokered service. Then create our
+ // wrapper CloudCacheService directly on that instance.
+ var authorizationServiceClient = new AuthorizationServiceClient(new AuthorizationServiceMock());
+ var solutionService = new SolutionServiceMock();
+ var fileSystem = new FileSystemServiceMock();
+ var serviceBroker = new ServiceBrokerMock()
+ {
+ BrokeredServices =
+ {
+ { VisualStudioServices.VS2019_10.SolutionService.Moniker, solutionService },
+ { VisualStudioServices.VS2019_10.FileSystem.Moniker, fileSystem },
+ { FrameworkServices.Authorization.Moniker, new AuthorizationServiceMock() },
+ },
+ };
+
+ var someContext = new CacheContext { RelativePathBase = _relativePathBase };
+ var pool = new SqliteConnectionPool();
+ var activeContext = await pool.ActivateContextAsync(someContext, default);
+ var cacheService = new CacheService(activeContext, serviceBroker, authorizationServiceClient, pool);
+ return cacheService;
+ }
+ }
+}
diff --git a/src/VisualStudio/CSharp/Test/PersistentStorage/OptionServiceMock.cs b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/OptionServiceMock.cs
similarity index 97%
rename from src/VisualStudio/CSharp/Test/PersistentStorage/OptionServiceMock.cs
rename to src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/OptionServiceMock.cs
index 0af88b3beef93..caa140bd3db5b 100644
--- a/src/VisualStudio/CSharp/Test/PersistentStorage/OptionServiceMock.cs
+++ b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/OptionServiceMock.cs
@@ -12,7 +12,7 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Options;
-namespace Microsoft.CodeAnalysis.UnitTests.WorkspaceServices
+namespace Microsoft.CodeAnalysis.UnitTests.WorkspaceServices.Mocks
{
internal class OptionServiceMock : IOptionService
{
diff --git a/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/ServiceBrokerMock.cs b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/ServiceBrokerMock.cs
new file mode 100644
index 0000000000000..4d271102005dd
--- /dev/null
+++ b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/ServiceBrokerMock.cs
@@ -0,0 +1,41 @@
+// 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.
+
+// Copy of https://devdiv.visualstudio.com/DevDiv/_git/VS.CloudCache?path=%2Ftest%2FMicrosoft.VisualStudio.Cache.Tests%2FMocks&_a=contents&version=GBmain
+// Try to keep in sync and avoid unnecessary changes here.
+
+using System;
+using System.Collections.Generic;
+using System.IO.Pipelines;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.ServiceHub.Framework;
+
+namespace Microsoft.CodeAnalysis.UnitTests.WorkspaceServices.Mocks
+{
+ internal class ServiceBrokerMock : IServiceBroker
+ {
+ public event EventHandler? AvailabilityChanged;
+
+ internal Dictionary BrokeredServices { get; } = new();
+
+ public ValueTask GetPipeAsync(ServiceMoniker serviceMoniker, ServiceActivationOptions options = default, CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ValueTask GetProxyAsync(ServiceRpcDescriptor serviceDescriptor, ServiceActivationOptions options = default, CancellationToken cancellationToken = default)
+ where T : class
+ {
+ if (this.BrokeredServices.TryGetValue(serviceDescriptor.Moniker, out object? service))
+ {
+ return new((T?)service);
+ }
+
+ return default;
+ }
+
+ internal void OnAvailabilityChanged(BrokeredServicesChangedEventArgs args) => this.AvailabilityChanged?.Invoke(this, args);
+ }
+}
diff --git a/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/SolutionServiceMock.cs b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/SolutionServiceMock.cs
new file mode 100644
index 0000000000000..0819ce75e36df
--- /dev/null
+++ b/src/VisualStudio/CSharp/Test/PersistentStorage/Mocks/SolutionServiceMock.cs
@@ -0,0 +1,104 @@
+// 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.
+
+// Copy of https://devdiv.visualstudio.com/DevDiv/_git/VS.CloudCache?path=%2Ftest%2FMicrosoft.VisualStudio.Cache.Tests%2FMocks&_a=contents&version=GBmain
+// Try to keep in sync and avoid unnecessary changes here.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Threading.Tasks.Dataflow;
+using Microsoft.VisualStudio.RpcContracts.Solution;
+using Microsoft.VisualStudio.Threading;
+
+#pragma warning disable CS0067 // events that are never used
+
+namespace Microsoft.CodeAnalysis.UnitTests.WorkspaceServices.Mocks
+{
+ internal class SolutionServiceMock : ISolutionService
+ {
+ private readonly BroadcastObservable openContainerObservable = new BroadcastObservable(new OpenCodeContainersState());
+
+ public event EventHandler? ProjectsLoaded;
+
+ public event EventHandler? ProjectsUnloaded;
+
+ public event EventHandler? ProjectLoadProgressChanged;
+
+ internal Uri? SolutionFilePath
+ {
+ get => this.openContainerObservable.Value.SolutionFilePath;
+ set => this.openContainerObservable.Value = this.openContainerObservable.Value with { SolutionFilePath = value };
+ }
+
+ public ValueTask AreProjectsLoadedAsync(Guid[] projectIds, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task SubscribeToOpenCodeContainersStateAsync(IObserver observer, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return Task.FromResult(this.openContainerObservable.Subscribe(observer));
+ }
+
+ public Task GetOpenCodeContainersStateAsync(CancellationToken cancellationToken) => Task.FromResult(this.openContainerObservable.Value);
+
+ public Task CloseSolutionAndFolderAsync(CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public ValueTask> GetPropertyValuesAsync(IReadOnlyList propertyIds, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public ValueTask> GetSolutionTelemetryContextPropertyValuesAsync(IReadOnlyList propertyNames, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public ValueTask LoadProjectsAsync(Guid[] projectIds, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public ValueTask LoadProjectsWithResultAsync(Guid[] projectIds, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public ValueTask RemoveProjectsAsync(IReadOnlyList projectIds, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task RequestProjectEventsAsync(CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public Task SaveSolutionFilterFileAsync(string filterFileDirectory, string filterFileName, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ public ValueTask UnloadProjectsAsync(Guid[] projectIds, ProjectUnloadReason unloadReason, CancellationToken cancellationToken) => throw new NotImplementedException();
+
+ internal void SimulateFolderChange(IReadOnlyList folderPaths) => this.openContainerObservable.Value = this.openContainerObservable.Value with { OpenFolderPaths = folderPaths };
+
+ private class BroadcastObservable : IObservable
+ {
+ private readonly BroadcastBlock sourceBlock = new(v => v);
+ private T value;
+
+ internal BroadcastObservable(T initialValue)
+ {
+ this.sourceBlock.Post(this.value = initialValue);
+ }
+
+ internal T Value
+ {
+ get => this.value;
+ set => this.sourceBlock.Post(this.value = value);
+ }
+
+ public IDisposable Subscribe(IObserver observer)
+ {
+ var actionBlock = new ActionBlock(observer.OnNext);
+ actionBlock.Completion.ContinueWith(
+ static (t, s) =>
+ {
+ var observer = (IObserver)s!;
+ if (t.Exception is object)
+ {
+ observer.OnError(t.Exception);
+ }
+ else
+ {
+ observer.OnCompleted();
+ }
+ },
+ observer,
+ TaskScheduler.Default).Forget();
+ return this.sourceBlock.LinkTo(actionBlock, new DataflowLinkOptions { PropagateCompletion = true });
+ }
+ }
+ }
+}
diff --git a/src/VisualStudio/CSharp/Test/PersistentStorage/SQLiteV2PersistentStorageTests.cs b/src/VisualStudio/CSharp/Test/PersistentStorage/SQLiteV2PersistentStorageTests.cs
index f05948bfd630a..06976afc94576 100644
--- a/src/VisualStudio/CSharp/Test/PersistentStorage/SQLiteV2PersistentStorageTests.cs
+++ b/src/VisualStudio/CSharp/Test/PersistentStorage/SQLiteV2PersistentStorageTests.cs
@@ -8,6 +8,7 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
+using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.SQLite.v2;
using Microsoft.CodeAnalysis.Storage;
using Xunit;
@@ -21,8 +22,8 @@ namespace Microsoft.CodeAnalysis.UnitTests.WorkspaceServices
///
public class SQLiteV2PersistentStorageTests : AbstractPersistentStorageTests
{
- internal override AbstractPersistentStorageService GetStorageService(IMefHostExportProvider exportProvider, IPersistentStorageLocationService locationService, IPersistentStorageFaultInjector? faultInjector)
- => new SQLitePersistentStorageService(exportProvider.GetExports().Single().Value, locationService, faultInjector);
+ internal override AbstractPersistentStorageService GetStorageService(OptionSet options, IMefHostExportProvider exportProvider, IPersistentStorageLocationService locationService, IPersistentStorageFaultInjector? faultInjector, string relativePathBase)
+ => new SQLitePersistentStorageService(options, exportProvider.GetExports().Single().Value, locationService, faultInjector);
[Fact]
public async Task TestCrashInNewConnection()
diff --git a/src/VisualStudio/Core/Def/Storage/AbstractCloudCachePersistentStorageService.cs b/src/VisualStudio/Core/Def/Storage/AbstractCloudCachePersistentStorageService.cs
new file mode 100644
index 0000000000000..db39f0747d521
--- /dev/null
+++ b/src/VisualStudio/Core/Def/Storage/AbstractCloudCachePersistentStorageService.cs
@@ -0,0 +1,54 @@
+// 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.Host;
+using Microsoft.CodeAnalysis.PersistentStorage;
+using Microsoft.CodeAnalysis.Storage;
+using Microsoft.VisualStudio.RpcContracts.Caching;
+using Roslyn.Utilities;
+
+namespace Microsoft.VisualStudio.LanguageServices.Storage
+{
+ internal abstract class AbstractCloudCachePersistentStorageService : AbstractPersistentStorageService
+ {
+ private const string StorageExtension = "CloudCache";
+
+ protected AbstractCloudCachePersistentStorageService(
+ IPersistentStorageLocationService locationService)
+ : base(locationService)
+ {
+ }
+
+ protected abstract void DisposeCacheService(ICacheService cacheService);
+ protected abstract ValueTask CreateCacheServiceAsync(CancellationToken cancellationToken);
+
+ protected sealed override string GetDatabaseFilePath(string workingFolderPath)
+ {
+ Contract.ThrowIfTrue(string.IsNullOrWhiteSpace(workingFolderPath));
+ return Path.Combine(workingFolderPath, StorageExtension);
+ }
+
+ protected sealed override bool ShouldDeleteDatabase(Exception exception)
+ {
+ // CloudCache owns the db, so we don't have to delete anything ourselves.
+ return false;
+ }
+
+ protected sealed override async ValueTask TryOpenDatabaseAsync(
+ SolutionKey solutionKey, string workingFolderPath, string databaseFilePath, CancellationToken cancellationToken)
+ {
+ var cacheService = await this.CreateCacheServiceAsync(cancellationToken).ConfigureAwait(false);
+ var relativePathBase = await cacheService.GetRelativePathBaseAsync(cancellationToken).ConfigureAwait(false);
+ if (string.IsNullOrEmpty(relativePathBase))
+ return null;
+
+ return new CloudCachePersistentStorage(
+ cacheService, solutionKey, workingFolderPath, relativePathBase, databaseFilePath, this.DisposeCacheService);
+ }
+ }
+}
diff --git a/src/VisualStudio/Core/Def/Storage/CloudCachePersistentStorage.cs b/src/VisualStudio/Core/Def/Storage/CloudCachePersistentStorage.cs
new file mode 100644
index 0000000000000..d538d67739f67
--- /dev/null
+++ b/src/VisualStudio/Core/Def/Storage/CloudCachePersistentStorage.cs
@@ -0,0 +1,213 @@
+// 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.IO.Pipelines;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Host;
+using Microsoft.CodeAnalysis.PersistentStorage;
+using Microsoft.CodeAnalysis.PooledObjects;
+using Microsoft.VisualStudio.RpcContracts.Caching;
+using Roslyn.Utilities;
+
+namespace Microsoft.VisualStudio.LanguageServices.Storage
+{
+ ///
+ /// Implementation of Roslyn's sitting on top of the platform's cloud storage
+ /// system.
+ ///
+ internal class CloudCachePersistentStorage : AbstractPersistentStorage
+ {
+ private static readonly ObjectPool s_byteArrayPool = new(() => new byte[Checksum.HashSize]);
+
+ ///
+ /// We do not need to store anything specific about the solution in this key as the platform cloud cache is
+ /// already keyed to the current solution. So this just allows us to store values considering that as the root.
+ ///
+ private static readonly CacheContainerKey s_solutionKey = new("Roslyn.Solution");
+
+ ///
+ /// Cache from project green nodes to the container keys we've computed for it (and the documents inside of it).
+ /// We can avoid computing these container keys when called repeatedly for the same projects/documents.
+ ///
+ private static readonly ConditionalWeakTable s_projectToContainerKeyCache = new();
+ private readonly ConditionalWeakTable.CreateValueCallback _projectToContainerKeyCacheCallback;
+
+ ///
+ /// Underlying cache service (owned by platform team) responsible for actual storage and retrieval of data.
+ ///
+ private readonly ICacheService _cacheService;
+ private readonly Action _disposeCacheService;
+
+ public CloudCachePersistentStorage(
+ ICacheService cacheService,
+ SolutionKey solutionKey,
+ string workingFolderPath,
+ string relativePathBase,
+ string databaseFilePath,
+ Action disposeCacheService)
+ : base(workingFolderPath, relativePathBase, databaseFilePath)
+ {
+ _cacheService = cacheService;
+ _disposeCacheService = disposeCacheService;
+ _projectToContainerKeyCacheCallback = ps => new ProjectContainerKeyCache(relativePathBase, ProjectKey.ToProjectKey(solutionKey, ps));
+ }
+
+ public sealed override void Dispose()
+ => _disposeCacheService(_cacheService);
+
+ public sealed override ValueTask DisposeAsync()
+ {
+ if (this._cacheService is IAsyncDisposable asyncDisposable)
+ {
+ return asyncDisposable.DisposeAsync();
+ }
+ else if (this._cacheService is IDisposable disposable)
+ {
+ disposable.Dispose();
+ return ValueTaskFactory.CompletedTask;
+ }
+
+ return ValueTaskFactory.CompletedTask;
+ }
+
+ ///
+ /// Maps our own roslyn key to the appropriate key to use for the cloud cache system. To avoid lots of
+ /// allocations we cache these (weakly) so if the same keys are used we can use the same platform keys.
+ ///
+ private CacheContainerKey? GetContainerKey(ProjectKey projectKey, Project? project)
+ {
+ return project != null
+ ? s_projectToContainerKeyCache.GetValue(project.State, _projectToContainerKeyCacheCallback).ProjectContainerKey
+ : ProjectContainerKeyCache.CreateProjectContainerKey(this.SolutionFilePath, projectKey);
+ }
+
+ ///
+ /// Maps our own roslyn key to the appropriate key to use for the cloud cache system. To avoid lots of
+ /// allocations we cache these (weakly) so if the same keys are used we can use the same platform keys.
+ ///
+ private CacheContainerKey? GetContainerKey(
+ DocumentKey documentKey, Document? document)
+ {
+ return document != null
+ ? s_projectToContainerKeyCache.GetValue(document.Project.State, _projectToContainerKeyCacheCallback).GetDocumentContainerKey(document.State)
+ : ProjectContainerKeyCache.CreateDocumentContainerKey(this.SolutionFilePath, documentKey);
+ }
+
+ public sealed override Task ChecksumMatchesAsync(string name, Checksum checksum, CancellationToken cancellationToken)
+ => ChecksumMatchesAsync(name, checksum, s_solutionKey, cancellationToken);
+
+ protected sealed override Task ChecksumMatchesAsync(ProjectKey projectKey, Project? project, string name, Checksum checksum, CancellationToken cancellationToken)
+ => ChecksumMatchesAsync(name, checksum, GetContainerKey(projectKey, project), cancellationToken);
+
+ protected sealed override Task ChecksumMatchesAsync(DocumentKey documentKey, Document? document, string name, Checksum checksum, CancellationToken cancellationToken)
+ => ChecksumMatchesAsync(name, checksum, GetContainerKey(documentKey, document), cancellationToken);
+
+ private async Task ChecksumMatchesAsync(string name, Checksum checksum, CacheContainerKey? containerKey, CancellationToken cancellationToken)
+ {
+ // If we failed to get a container key (for example, because the client is referencing a file not under the
+ // solution folder) then we can't proceed.
+ if (containerKey == null)
+ return false;
+
+ using var bytes = s_byteArrayPool.GetPooledObject();
+ checksum.WriteTo(bytes.Object);
+
+ return await _cacheService.CheckExistsAsync(new CacheItemKey(containerKey.Value, name) { Version = bytes.Object }, cancellationToken).ConfigureAwait(false);
+ }
+
+ public sealed override Task ReadStreamAsync(string name, Checksum? checksum, CancellationToken cancellationToken)
+ => ReadStreamAsync(name, checksum, s_solutionKey, cancellationToken);
+
+ protected sealed override Task ReadStreamAsync(ProjectKey projectKey, Project? project, string name, Checksum? checksum, CancellationToken cancellationToken)
+ => ReadStreamAsync(name, checksum, GetContainerKey(projectKey, project), cancellationToken);
+
+ protected sealed override Task ReadStreamAsync(DocumentKey documentKey, Document? document, string name, Checksum? checksum, CancellationToken cancellationToken)
+ => ReadStreamAsync(name, checksum, GetContainerKey(documentKey, document), cancellationToken);
+
+ private async Task ReadStreamAsync(string name, Checksum? checksum, CacheContainerKey? containerKey, CancellationToken cancellationToken)
+ {
+ // If we failed to get a container key (for example, because the client is referencing a file not under the
+ // solution folder) then we can't proceed.
+ if (containerKey == null)
+ return null;
+
+ if (checksum == null)
+ {
+ return await ReadStreamAsync(new CacheItemKey(containerKey.Value, name), cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ using var bytes = s_byteArrayPool.GetPooledObject();
+ checksum.WriteTo(bytes.Object);
+
+ return await ReadStreamAsync(new CacheItemKey(containerKey.Value, name) { Version = bytes.Object }, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ private async Task ReadStreamAsync(CacheItemKey key, CancellationToken cancellationToken)
+ {
+ var pipe = new Pipe();
+ var result = await _cacheService.TryGetItemAsync(key, pipe.Writer, cancellationToken).ConfigureAwait(false);
+ if (!result)
+ return null;
+
+ // Clients will end up doing blocking reads on the synchronous stream we return from this. This can
+ // negatively impact our calls as that will cause sync blocking on the async work to fill the pipe. To
+ // alleviate that issue, we actually asynchronously read in the entire stream into memory inside the reader
+ // and then pass that out. This should not be a problem in practice as PipeReader internally intelligently
+ // uses and pools reasonable sized buffers, preventing us from exacerbating the GC or causing LOH
+ // allocations.
+ while (true)
+ {
+ var readResult = await pipe.Reader.ReadAsync(cancellationToken).ConfigureAwait(false);
+ pipe.Reader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End);
+
+ if (readResult.IsCompleted)
+ break;
+ }
+
+ return pipe.Reader.AsStream();
+ }
+
+ public sealed override Task WriteStreamAsync(string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken)
+ => WriteStreamAsync(name, stream, checksum, s_solutionKey, cancellationToken);
+
+ protected sealed override Task WriteStreamAsync(ProjectKey projectKey, Project? project, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken)
+ => WriteStreamAsync(name, stream, checksum, GetContainerKey(projectKey, project), cancellationToken);
+
+ protected sealed override Task WriteStreamAsync(DocumentKey documentKey, Document? document, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken)
+ => WriteStreamAsync(name, stream, checksum, GetContainerKey(documentKey, document), cancellationToken);
+
+ private async Task WriteStreamAsync(string name, Stream stream, Checksum? checksum, CacheContainerKey? containerKey, CancellationToken cancellationToken)
+ {
+ // If we failed to get a container key (for example, because the client is referencing a file not under the
+ // solution folder) then we can't proceed.
+ if (containerKey == null)
+ return false;
+
+ if (checksum == null)
+ {
+ return await WriteStreamAsync(new CacheItemKey(containerKey.Value, name), stream, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ using var bytes = s_byteArrayPool.GetPooledObject();
+ checksum.WriteTo(bytes.Object);
+
+ return await WriteStreamAsync(new CacheItemKey(containerKey.Value, name) { Version = bytes.Object }, stream, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ private async Task WriteStreamAsync(CacheItemKey key, Stream stream, CancellationToken cancellationToken)
+ {
+ await _cacheService.SetItemAsync(key, PipeReader.Create(stream), shareable: false, cancellationToken).ConfigureAwait(false);
+ return true;
+ }
+ }
+}
diff --git a/src/VisualStudio/Core/Def/Storage/ProjectContainerKeyCache.cs b/src/VisualStudio/Core/Def/Storage/ProjectContainerKeyCache.cs
new file mode 100644
index 0000000000000..2d8bbc75d725d
--- /dev/null
+++ b/src/VisualStudio/Core/Def/Storage/ProjectContainerKeyCache.cs
@@ -0,0 +1,110 @@
+// 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.Collections.Generic;
+using System.Collections.Immutable;
+using System.Runtime.CompilerServices;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.PersistentStorage;
+using Microsoft.VisualStudio.RpcContracts.Caching;
+using Roslyn.Utilities;
+
+namespace Microsoft.VisualStudio.LanguageServices.Storage
+{
+ ///
+ /// Cache of our own internal roslyn storage keys to the equivalent platform cloud cache keys. Cloud cache keys can
+ /// store a lot of date in them (like their 'dimensions' dictionary. We don't want to continually recreate these as
+ /// we read/write date to the db.
+ ///
+ internal class ProjectContainerKeyCache
+ {
+ private static readonly ImmutableSortedDictionary EmptyDimensions = ImmutableSortedDictionary.Create(StringComparer.Ordinal);
+
+ ///
+ /// Container key explicitly for the project itself.
+ ///
+ public readonly CacheContainerKey? ProjectContainerKey;
+
+ ///
+ /// Cache from document green nodes to the container keys we've computed for it. We can avoid computing these
+ /// container keys when called repeatedly for the same documents.
+ ///
+ ///
+ /// We can use a normal Dictionary here instead of a as
+ /// instances of are always owned in a context where the is alive. As that instance is alive, all s the project
+ /// points at will be held alive strongly too.
+ ///
+ private readonly Dictionary _documentToContainerKey = new();
+ private readonly Func _documentToContainerKeyCallback;
+
+ public ProjectContainerKeyCache(string relativePathBase, ProjectKey projectKey)
+ {
+ ProjectContainerKey = CreateProjectContainerKey(relativePathBase, projectKey);
+
+ _documentToContainerKeyCallback = ds => CreateDocumentContainerKey(relativePathBase, DocumentKey.ToDocumentKey(projectKey, ds));
+ }
+
+ public CacheContainerKey? GetDocumentContainerKey(TextDocumentState state)
+ {
+ lock (_documentToContainerKey)
+ return _documentToContainerKey.GetOrAdd(state, _documentToContainerKeyCallback);
+ }
+
+ public static CacheContainerKey? CreateProjectContainerKey(
+ string relativePathBase, ProjectKey projectKey)
+ {
+ // Creates a container key for this project. The container key is a mix of the project's name, relative
+ // file path (to the solution), and optional parse options.
+
+ // If we don't have a valid solution path, we can't store anything.
+ if (string.IsNullOrEmpty(relativePathBase))
+ return null;
+
+ // We have to have a file path for this project
+ if (RoslynString.IsNullOrEmpty(projectKey.FilePath))
+ return null;
+
+ // The file path has to be relative to the base path the DB is associated with (either the solution-path or
+ // repo-path).
+ var relativePath = PathUtilities.GetRelativePath(relativePathBase, projectKey.FilePath!);
+ if (relativePath == projectKey.FilePath)
+ return null;
+
+ var dimensions = EmptyDimensions
+ .Add($"{nameof(ProjectKey)}.{nameof(ProjectKey.Name)}", projectKey.Name)
+ .Add($"{nameof(ProjectKey)}.{nameof(ProjectKey.FilePath)}", relativePath)
+ .Add($"{nameof(ProjectKey)}.{nameof(ProjectKey.ParseOptionsChecksum)}", projectKey.ParseOptionsChecksum.ToString());
+
+ return new CacheContainerKey("Roslyn.Project", dimensions);
+ }
+
+ public static CacheContainerKey? CreateDocumentContainerKey(
+ string relativePathBase,
+ DocumentKey documentKey)
+ {
+ // See if we can get a project key for this info. If not, we def can't get a doc key.
+ var projectContainerKey = CreateProjectContainerKey(relativePathBase, documentKey.Project);
+ if (projectContainerKey == null)
+ return null;
+
+ // We have to have a file path for this document
+ if (string.IsNullOrEmpty(documentKey.FilePath))
+ return null;
+
+ // The file path has to be relative to the base path the DB is associated with (either the solution-path or
+ // repo-path).
+ var relativePath = PathUtilities.GetRelativePath(relativePathBase, documentKey.FilePath!);
+ if (relativePath == documentKey.FilePath)
+ return null;
+
+ var dimensions = projectContainerKey.Value.Dimensions
+ .Add($"{nameof(DocumentKey)}.{nameof(DocumentKey.Name)}", documentKey.Name)
+ .Add($"{nameof(DocumentKey)}.{nameof(DocumentKey.FilePath)}", relativePath);
+
+ return new CacheContainerKey("Roslyn.Document", dimensions);
+ }
+ }
+}
diff --git a/src/VisualStudio/Core/Def/Storage/VisualStudioCloudCacheStorageService.cs b/src/VisualStudio/Core/Def/Storage/VisualStudioCloudCacheStorageService.cs
new file mode 100644
index 0000000000000..6ef9fa3714c54
--- /dev/null
+++ b/src/VisualStudio/Core/Def/Storage/VisualStudioCloudCacheStorageService.cs
@@ -0,0 +1,56 @@
+// 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.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
+using Microsoft.CodeAnalysis.Host;
+using Microsoft.VisualStudio.RpcContracts.Caching;
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio.Shell.ServiceBroker;
+using Roslyn.Utilities;
+
+namespace Microsoft.VisualStudio.LanguageServices.Storage
+{
+ internal class VisualStudioCloudCacheStorageService : AbstractCloudCachePersistentStorageService
+ {
+ private readonly IAsyncServiceProvider _serviceProvider;
+ private readonly IThreadingContext _threadingContext;
+
+ public VisualStudioCloudCacheStorageService(IAsyncServiceProvider serviceProvider, IThreadingContext threadingContext, IPersistentStorageLocationService locationService)
+ : base(locationService)
+ {
+ _serviceProvider = serviceProvider;
+ _threadingContext = threadingContext;
+ }
+
+ protected sealed override void DisposeCacheService(ICacheService cacheService)
+ {
+ if (cacheService is IAsyncDisposable asyncDisposable)
+ {
+ _threadingContext.JoinableTaskFactory.Run(
+ () => asyncDisposable.DisposeAsync().AsTask());
+ }
+ else if (cacheService is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ }
+
+ protected sealed override async ValueTask CreateCacheServiceAsync(CancellationToken cancellationToken)
+ {
+ var serviceContainer = await _serviceProvider.GetServiceAsync().ConfigureAwait(false);
+ var serviceBroker = serviceContainer.GetFullAccessServiceBroker();
+
+#pragma warning disable ISB001 // Dispose of proxies
+ // cache service will be disposed inside VisualStudioCloudCachePersistentStorage.Dispose
+ var cacheService = await serviceBroker.GetProxyAsync(VisualStudioServices.VS2019_10.CacheService, cancellationToken: cancellationToken).ConfigureAwait(false);
+#pragma warning restore ISB001 // Dispose of proxies
+
+ Contract.ThrowIfNull(cacheService);
+ return cacheService;
+ }
+ }
+}
diff --git a/src/VisualStudio/Core/Def/Storage/VisualStudioCloudCacheStorageServiceFactory.cs b/src/VisualStudio/Core/Def/Storage/VisualStudioCloudCacheStorageServiceFactory.cs
new file mode 100644
index 0000000000000..da2e9afbae3de
--- /dev/null
+++ b/src/VisualStudio/Core/Def/Storage/VisualStudioCloudCacheStorageServiceFactory.cs
@@ -0,0 +1,35 @@
+// 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.Composition;
+using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
+using Microsoft.CodeAnalysis.Host;
+using Microsoft.CodeAnalysis.Host.Mef;
+using Microsoft.CodeAnalysis.Storage;
+using Microsoft.CodeAnalysis.Storage.CloudCache;
+using Microsoft.VisualStudio.Shell;
+
+namespace Microsoft.VisualStudio.LanguageServices.Storage
+{
+ [ExportWorkspaceService(typeof(ICloudCacheStorageServiceFactory), ServiceLayer.Host), Shared]
+ internal class VisualStudioCloudCacheStorageServiceFactory : ICloudCacheStorageServiceFactory
+ {
+ private readonly IAsyncServiceProvider _serviceProvider;
+ private readonly IThreadingContext _threadingContext;
+
+ [ImportingConstructor]
+ [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
+ public VisualStudioCloudCacheStorageServiceFactory(
+ IThreadingContext threadingContext,
+ SVsServiceProvider serviceProvider)
+ {
+ _threadingContext = threadingContext;
+ _serviceProvider = (IAsyncServiceProvider)serviceProvider;
+ }
+
+ public AbstractPersistentStorageService Create(IPersistentStorageLocationService locationService)
+ => new VisualStudioCloudCacheStorageService(_serviceProvider, _threadingContext, locationService);
+ }
+}
diff --git a/src/Workspaces/Core/Portable/Storage/AbstractPersistentStorageService.cs b/src/Workspaces/Core/Portable/Storage/AbstractPersistentStorageService.cs
index 75d04b04128e8..854f5d100fac7 100644
--- a/src/Workspaces/Core/Portable/Storage/AbstractPersistentStorageService.cs
+++ b/src/Workspaces/Core/Portable/Storage/AbstractPersistentStorageService.cs
@@ -39,7 +39,7 @@ protected AbstractPersistentStorageService(IPersistentStorageLocationService loc
/// to delete the database and retry opening one more time. If that fails again, the instance will be used.
///
- protected abstract ValueTask TryOpenDatabaseAsync(SolutionKey solutionKey, string workingFolderPath, string databaseFilePath);
+ protected abstract ValueTask TryOpenDatabaseAsync(SolutionKey solutionKey, string workingFolderPath, string databaseFilePath, CancellationToken cancellationToken);
protected abstract bool ShouldDeleteDatabase(Exception exception);
[Obsolete("Use GetStorageAsync instead")]
@@ -62,9 +62,7 @@ public ValueTask GetStorageAsync(
Workspace workspace, SolutionKey solutionKey, Solution? bulkLoadSnapshot, bool checkBranchId, CancellationToken cancellationToken)
{
if (!DatabaseSupported(solutionKey, checkBranchId))
- {
- return new(NoOpPersistentStorage.Instance);
- }
+ return new(NoOpPersistentStorage.GetOrThrow(workspace.Options));
return GetStorageWorkerAsync(workspace, solutionKey, bulkLoadSnapshot, cancellationToken);
}
@@ -84,7 +82,7 @@ internal async ValueTask GetStorageWorkerAsync(
var workingFolder = TryGetWorkingFolder(workspace, solutionKey, bulkLoadSnapshot);
if (workingFolder == null)
- return NoOpPersistentStorage.Instance;
+ return NoOpPersistentStorage.GetOrThrow(workspace.Options);
// If we already had some previous cached service, let's let it start cleaning up
if (_currentPersistentStorage != null)
@@ -101,7 +99,7 @@ internal async ValueTask GetStorageWorkerAsync(
_currentPersistentStorageSolutionId = null;
}
- var storage = await CreatePersistentStorageAsync(solutionKey, workingFolder).ConfigureAwait(false);
+ var storage = await CreatePersistentStorageAsync(workspace, solutionKey, workingFolder, cancellationToken).ConfigureAwait(false);
Contract.ThrowIfNull(storage);
// Create and cache a new storage instance associated with this particular solution.
@@ -145,23 +143,32 @@ private static bool DatabaseSupported(SolutionKey solution, bool checkBranchId)
return true;
}
- private async ValueTask CreatePersistentStorageAsync(SolutionKey solutionKey, string workingFolderPath)
+ private async ValueTask CreatePersistentStorageAsync(
+ Workspace workspace, 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.
- return await TryCreatePersistentStorageAsync(solutionKey, workingFolderPath).ConfigureAwait(false) ??
- await TryCreatePersistentStorageAsync(solutionKey, workingFolderPath).ConfigureAwait(false) ??
- NoOpPersistentStorage.Instance;
+ var result = await TryCreatePersistentStorageAsync(workspace, solutionKey, workingFolderPath, cancellationToken).ConfigureAwait(false) ??
+ await TryCreatePersistentStorageAsync(workspace, solutionKey, workingFolderPath, cancellationToken).ConfigureAwait(false);
+
+ if (result != null)
+ return result;
+
+ return NoOpPersistentStorage.GetOrThrow(workspace.Options);
}
- private async ValueTask TryCreatePersistentStorageAsync(SolutionKey solutionKey, string workingFolderPath)
+ private async ValueTask TryCreatePersistentStorageAsync(
+ Workspace workspace,
+ SolutionKey solutionKey,
+ string workingFolderPath,
+ CancellationToken cancellationToken)
{
var databaseFilePath = GetDatabaseFilePath(workingFolderPath);
try
{
- return await TryOpenDatabaseAsync(solutionKey, workingFolderPath, databaseFilePath).ConfigureAwait(false);
+ return await TryOpenDatabaseAsync(solutionKey, workingFolderPath, databaseFilePath, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -175,6 +182,9 @@ await TryCreatePersistentStorageAsync(solutionKey, workingFolderPath).ConfigureA
IOUtilities.PerformIO(() => Directory.Delete(Path.GetDirectoryName(databaseFilePath)!, recursive: true));
}
+ if (workspace.Options.GetOption(StorageOptions.DatabaseMustSucceed))
+ throw;
+
return null;
}
}
diff --git a/src/Workspaces/Core/Portable/Storage/CloudCache/ICloudCacheStorageServiceFactory.cs b/src/Workspaces/Core/Portable/Storage/CloudCache/ICloudCacheStorageServiceFactory.cs
new file mode 100644
index 0000000000000..a0a168a7dd385
--- /dev/null
+++ b/src/Workspaces/Core/Portable/Storage/CloudCache/ICloudCacheStorageServiceFactory.cs
@@ -0,0 +1,13 @@
+// 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 Microsoft.CodeAnalysis.Host;
+
+namespace Microsoft.CodeAnalysis.Storage.CloudCache
+{
+ internal interface ICloudCacheStorageServiceFactory : IWorkspaceService
+ {
+ AbstractPersistentStorageService Create(IPersistentStorageLocationService locationService);
+ }
+}
diff --git a/src/Workspaces/Core/Portable/Storage/DesktopPersistenceStorageServiceFactory.cs b/src/Workspaces/Core/Portable/Storage/DesktopPersistenceStorageServiceFactory.cs
new file mode 100644
index 0000000000000..4c83680f15ffb
--- /dev/null
+++ b/src/Workspaces/Core/Portable/Storage/DesktopPersistenceStorageServiceFactory.cs
@@ -0,0 +1,74 @@
+// 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.Composition;
+using Microsoft.CodeAnalysis.Host;
+using Microsoft.CodeAnalysis.Host.Mef;
+using Microsoft.CodeAnalysis.Options;
+
+// When building for source-build, there is no sqlite dependency
+#if !DOTNET_BUILD_FROM_SOURCE
+using Microsoft.CodeAnalysis.SQLite.v2;
+using Microsoft.CodeAnalysis.Storage.CloudCache;
+#endif
+
+namespace Microsoft.CodeAnalysis.Storage
+{
+ [ExportWorkspaceServiceFactory(typeof(IPersistentStorageService), ServiceLayer.Desktop), Shared]
+ internal class DesktopPersistenceStorageServiceFactory : IWorkspaceServiceFactory
+ {
+#if DOTNET_BUILD_FROM_SOURCE
+
+ [ImportingConstructor]
+ [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
+ public DesktopPersistenceStorageServiceFactory()
+ {
+ }
+
+ public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices)
+ {
+ return NoOpPersistentStorageService.Instance;
+ }
+
+#else
+
+ private readonly SQLiteConnectionPoolService _connectionPoolService;
+
+ [ImportingConstructor]
+ [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
+ public DesktopPersistenceStorageServiceFactory(SQLiteConnectionPoolService connectionPoolService)
+ {
+ _connectionPoolService = connectionPoolService;
+ }
+
+ public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices)
+ {
+ var optionService = workspaceServices.GetRequiredService();
+ var database = optionService.GetOption(StorageOptions.Database);
+ var options = workspaceServices.Workspace.Options;
+
+ var locationService = workspaceServices.GetService();
+ if (locationService != null)
+ {
+ switch (database)
+ {
+ case StorageDatabase.SQLite:
+ return new SQLitePersistentStorageService(options, _connectionPoolService, locationService);
+
+ case StorageDatabase.CloudCache:
+ var factory = workspaceServices.GetService();
+
+ return factory == null
+ ? NoOpPersistentStorageService.GetOrThrow(options)
+ : factory.Create(locationService);
+ }
+ }
+
+ return NoOpPersistentStorageService.GetOrThrow(options);
+ }
+
+#endif
+ }
+}
diff --git a/src/Workspaces/Core/Portable/Storage/PersistenceStorageServiceFactory.cs b/src/Workspaces/Core/Portable/Storage/PersistenceStorageServiceFactory.cs
deleted file mode 100644
index 7631e5fa81d25..0000000000000
--- a/src/Workspaces/Core/Portable/Storage/PersistenceStorageServiceFactory.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-// 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.Composition;
-using Microsoft.CodeAnalysis.Host;
-using Microsoft.CodeAnalysis.Host.Mef;
-using Microsoft.CodeAnalysis.Options;
-
-// When building for source-build, there is no sqlite dependency
-#if !DOTNET_BUILD_FROM_SOURCE
-using Microsoft.CodeAnalysis.SQLite.v2;
-#endif
-
-namespace Microsoft.CodeAnalysis.Storage
-{
- [ExportWorkspaceServiceFactory(typeof(IPersistentStorageService), ServiceLayer.Desktop), Shared]
- internal class PersistenceStorageServiceFactory : IWorkspaceServiceFactory
- {
-#if !DOTNET_BUILD_FROM_SOURCE
- private readonly SQLiteConnectionPoolService _connectionPoolService;
-#endif
-
- [ImportingConstructor]
- [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
- public PersistenceStorageServiceFactory(
-#if !DOTNET_BUILD_FROM_SOURCE
- SQLiteConnectionPoolService connectionPoolService
-#endif
- )
- {
-#if !DOTNET_BUILD_FROM_SOURCE
- _connectionPoolService = connectionPoolService;
-#endif
- }
-
- public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices)
- {
-#if !DOTNET_BUILD_FROM_SOURCE
- var optionService = workspaceServices.GetRequiredService();
- var database = optionService.GetOption(StorageOptions.Database);
- switch (database)
- {
- case StorageDatabase.SQLite:
- var locationService = workspaceServices.GetService();
- if (locationService != null)
- return new SQLite.v2.SQLitePersistentStorageService(_connectionPoolService, locationService);
-
- break;
- }
-#endif
-
- return NoOpPersistentStorageService.Instance;
- }
- }
-}
diff --git a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorageConstants.cs b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorageConstants.cs
index 77e5db7694bdc..788a8d516ed28 100644
--- a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorageConstants.cs
+++ b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorageConstants.cs
@@ -59,7 +59,7 @@ internal static class SQLitePersistentStorageConstants
///
/// Inside the DB we have a table for data that we want associated with a .
/// The data is keyed off of an integral value produced by combining the ID of the Project and
- /// the ID of the name of the data (see .
+ /// the ID of the name of the data (see .
///
/// This gives a very efficient integral key, and means that the we only have to store a
/// single mapping from stream name to ID in the string table.
@@ -76,7 +76,7 @@ internal static class SQLitePersistentStorageConstants
///
/// Inside the DB we have a table for data that we want associated with a .
/// The data is keyed off of an integral value produced by combining the ID of the Document and
- /// the ID of the name of the data (see .
+ /// the ID of the name of the data (see .
///
/// This gives a very efficient integral key, and means that the we only have to store a
/// single mapping from stream name to ID in the string table.
diff --git a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorageService.cs b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorageService.cs
index 7d6110b2c53a4..9baaf0089c3ba 100644
--- a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorageService.cs
+++ b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorageService.cs
@@ -4,8 +4,10 @@
using System;
using System.IO;
+using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
+using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PersistentStorage;
using Roslyn.Utilities;
@@ -17,19 +19,25 @@ internal class SQLitePersistentStorageService : AbstractSQLitePersistentStorageS
private const string PersistentStorageFileName = "storage.ide";
private readonly SQLiteConnectionPoolService _connectionPoolService;
+ private readonly OptionSet _options;
private readonly IPersistentStorageFaultInjector? _faultInjector;
- public SQLitePersistentStorageService(SQLiteConnectionPoolService connectionPoolService, IPersistentStorageLocationService locationService)
+ public SQLitePersistentStorageService(
+ OptionSet options,
+ SQLiteConnectionPoolService connectionPoolService,
+ IPersistentStorageLocationService locationService)
: base(locationService)
{
+ _options = options;
_connectionPoolService = connectionPoolService;
}
public SQLitePersistentStorageService(
+ OptionSet options,
SQLiteConnectionPoolService connectionPoolService,
IPersistentStorageLocationService locationService,
IPersistentStorageFaultInjector? faultInjector)
- : this(connectionPoolService, locationService)
+ : this(options, connectionPoolService, locationService)
{
_faultInjector = faultInjector;
}
@@ -41,7 +49,7 @@ protected override string GetDatabaseFilePath(string workingFolderPath)
}
protected override ValueTask TryOpenDatabaseAsync(
- SolutionKey solutionKey, string workingFolderPath, string databaseFilePath)
+ SolutionKey solutionKey, string workingFolderPath, string databaseFilePath, CancellationToken cancellationToken)
{
if (!TryInitializeLibraries())
{
@@ -49,6 +57,9 @@ protected override string GetDatabaseFilePath(string workingFolderPath)
return new((IChecksummedPersistentStorage?)null);
}
+ if (solutionKey.FilePath == null)
+ return new(NoOpPersistentStorage.GetOrThrow(_options));
+
return new(SQLitePersistentStorage.TryCreate(
_connectionPoolService,
workingFolderPath,
diff --git a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_DocumentSerialization.cs b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_DocumentSerialization.cs
index 0910ec4d5c45d..f214a364f2c82 100644
--- a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_DocumentSerialization.cs
+++ b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_DocumentSerialization.cs
@@ -14,13 +14,13 @@ namespace Microsoft.CodeAnalysis.SQLite.v2
internal partial class SQLitePersistentStorage
{
- public override Task ChecksumMatchesAsync(DocumentKey documentKey, string name, Checksum checksum, CancellationToken cancellationToken)
+ protected override Task ChecksumMatchesAsync(DocumentKey documentKey, Document? document, string name, Checksum checksum, CancellationToken cancellationToken)
=> _documentAccessor.ChecksumMatchesAsync((documentKey, name), checksum, cancellationToken);
- public override Task ReadStreamAsync(DocumentKey documentKey, string name, Checksum? checksum, CancellationToken cancellationToken)
+ protected override Task ReadStreamAsync(DocumentKey documentKey, Document? document, string name, Checksum? checksum, CancellationToken cancellationToken)
=> _documentAccessor.ReadStreamAsync((documentKey, name), checksum, cancellationToken);
- public override Task WriteStreamAsync(DocumentKey documentKey, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken)
+ protected override Task WriteStreamAsync(DocumentKey documentKey, Document? document, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken)
=> _documentAccessor.WriteStreamAsync((documentKey, name), stream, checksum, cancellationToken);
///
diff --git a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_ProjectSerialization.cs b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_ProjectSerialization.cs
index 01d76ce05645b..d5248c430280d 100644
--- a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_ProjectSerialization.cs
+++ b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_ProjectSerialization.cs
@@ -14,13 +14,13 @@ namespace Microsoft.CodeAnalysis.SQLite.v2
internal partial class SQLitePersistentStorage
{
- public override Task ChecksumMatchesAsync(ProjectKey projectKey, string name, Checksum checksum, CancellationToken cancellationToken)
+ protected override Task ChecksumMatchesAsync(ProjectKey projectKey, Project? project, string name, Checksum checksum, CancellationToken cancellationToken)
=> _projectAccessor.ChecksumMatchesAsync((projectKey, name), checksum, cancellationToken);
- public override Task ReadStreamAsync(ProjectKey projectKey, string name, Checksum? checksum, CancellationToken cancellationToken)
+ protected override Task ReadStreamAsync(ProjectKey projectKey, Project? project, string name, Checksum? checksum, CancellationToken cancellationToken)
=> _projectAccessor.ReadStreamAsync((projectKey, name), checksum, cancellationToken);
- public override Task WriteStreamAsync(ProjectKey projectKey, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken)
+ protected override Task WriteStreamAsync(ProjectKey projectKey, Project? project, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken)
=> _projectAccessor.WriteStreamAsync((projectKey, name), stream, checksum, cancellationToken);
///
diff --git a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_StringIds.cs b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_StringIds.cs
index 96c6da04b2b98..d927f3422677b 100644
--- a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_StringIds.cs
+++ b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_StringIds.cs
@@ -15,7 +15,7 @@ internal partial class SQLitePersistentStorage
{
private readonly ConcurrentDictionary _stringToIdMap = new();
- private int? TryGetStringId(SqlConnection connection, string value)
+ private int? TryGetStringId(SqlConnection connection, string? value)
{
// Null strings are not supported at all. Just ignore these. Any read/writes
// to null values will fail and will return 'false/null' to indicate failure
diff --git a/src/Workspaces/Core/Portable/Storage/StorageDatabase.cs b/src/Workspaces/Core/Portable/Storage/StorageDatabase.cs
index 4587658fe8c46..150f02a3469e8 100644
--- a/src/Workspaces/Core/Portable/Storage/StorageDatabase.cs
+++ b/src/Workspaces/Core/Portable/Storage/StorageDatabase.cs
@@ -8,5 +8,6 @@ internal enum StorageDatabase
{
None = 0,
SQLite = 1,
+ CloudCache = 2,
}
}
diff --git a/src/Workspaces/Core/Portable/Storage/StorageOptions.cs b/src/Workspaces/Core/Portable/Storage/StorageOptions.cs
index 39b908bbfd548..02147490ba64e 100644
--- a/src/Workspaces/Core/Portable/Storage/StorageOptions.cs
+++ b/src/Workspaces/Core/Portable/Storage/StorageOptions.cs
@@ -17,8 +17,15 @@ internal static class StorageOptions
public const string OptionName = "FeatureManager/Storage";
- public static readonly Option Database = new(
- OptionName, nameof(Database), defaultValue: StorageDatabase.SQLite);
+ public static readonly Option Database = new(OptionName, nameof(Database), defaultValue: StorageDatabase.SQLite);
+
+ ///
+ /// Option that can be set in certain scenarios (like tests) to indicate that the client expects the DB to
+ /// succeed at all work and that it should not ever gracefully fall over. Should not be set in normal host
+ /// environments, where it is completely reasonable for things to fail (for example, if a client asks for a key
+ /// that hasn't been stored yet).
+ ///
+ public static readonly Option DatabaseMustSucceed = new(OptionName, nameof(DatabaseMustSucceed), defaultValue: false);
}
[ExportOptionProvider, Shared]
@@ -31,6 +38,7 @@ public RemoteHostOptionsProvider()
}
public ImmutableArray Options { get; } = ImmutableArray.Create(
- StorageOptions.Database);
+ StorageOptions.Database,
+ StorageOptions.DatabaseMustSucceed);
}
}
diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/AbstractPersistentStorage.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/AbstractPersistentStorage.cs
index 8fe74c72239b2..80e423375e559 100644
--- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/AbstractPersistentStorage.cs
+++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/AbstractPersistentStorage.cs
@@ -41,24 +41,42 @@ protected AbstractPersistentStorage(
public abstract Task ReadStreamAsync(string name, Checksum? checksum, CancellationToken cancellationToken);
public abstract Task WriteStreamAsync(string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken);
- public abstract Task ChecksumMatchesAsync(ProjectKey projectKey, string name, Checksum checksum, CancellationToken cancellationToken);
- public abstract Task ChecksumMatchesAsync(DocumentKey documentKey, string name, Checksum checksum, CancellationToken cancellationToken);
- public abstract Task ReadStreamAsync(ProjectKey projectKey, string name, Checksum? checksum, CancellationToken cancellationToken);
- public abstract Task ReadStreamAsync(DocumentKey documentKey, string name, Checksum? checksum, CancellationToken cancellationToken);
- public abstract Task WriteStreamAsync(ProjectKey projectKey, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken);
- public abstract Task WriteStreamAsync(DocumentKey documentKey, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken);
+ protected abstract Task ChecksumMatchesAsync(ProjectKey projectKey, Project? project, string name, Checksum checksum, CancellationToken cancellationToken);
+ protected abstract Task ChecksumMatchesAsync(DocumentKey documentKey, Document? document, string name, Checksum checksum, CancellationToken cancellationToken);
+ protected abstract Task ReadStreamAsync(ProjectKey projectKey, Project? project, string name, Checksum? checksum, CancellationToken cancellationToken);
+ protected abstract Task ReadStreamAsync(DocumentKey documentKey, Document? document, string name, Checksum? checksum, CancellationToken cancellationToken);
+ protected abstract Task WriteStreamAsync(ProjectKey projectKey, Project? project, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken);
+ protected abstract Task WriteStreamAsync(DocumentKey documentKey, Document? document, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken);
+
+ public Task ChecksumMatchesAsync(ProjectKey projectKey, string name, Checksum checksum, CancellationToken cancellationToken)
+ => ChecksumMatchesAsync(projectKey, project: null, name, checksum, cancellationToken);
+
+ public Task ChecksumMatchesAsync(DocumentKey documentKey, string name, Checksum checksum, CancellationToken cancellationToken)
+ => ChecksumMatchesAsync(documentKey, document: null, name, checksum, cancellationToken);
+
+ public Task ReadStreamAsync(ProjectKey projectKey, string name, Checksum? checksum, CancellationToken cancellationToken)
+ => ReadStreamAsync(projectKey, project: null, name, checksum, cancellationToken);
+
+ public Task ReadStreamAsync(DocumentKey documentKey, string name, Checksum? checksum, CancellationToken cancellationToken)
+ => ReadStreamAsync(documentKey, document: null, name, checksum, cancellationToken);
+
+ public Task WriteStreamAsync(ProjectKey projectKey, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken)
+ => WriteStreamAsync(projectKey, project: null, name, stream, checksum, cancellationToken);
+
+ public Task WriteStreamAsync(DocumentKey documentKey, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken)
+ => WriteStreamAsync(documentKey, document: null, name, stream, checksum, cancellationToken);
public Task ChecksumMatchesAsync(Project project, string name, Checksum checksum, CancellationToken cancellationToken)
- => ChecksumMatchesAsync(ProjectKey.ToProjectKey(project), name, checksum, cancellationToken);
+ => ChecksumMatchesAsync(ProjectKey.ToProjectKey(project), project, name, checksum, cancellationToken);
public Task ChecksumMatchesAsync(Document document, string name, Checksum checksum, CancellationToken cancellationToken)
- => ChecksumMatchesAsync(DocumentKey.ToDocumentKey(document), name, checksum, cancellationToken);
+ => ChecksumMatchesAsync(DocumentKey.ToDocumentKey(document), document, name, checksum, cancellationToken);
public Task ReadStreamAsync(Project project, string name, Checksum? checksum, CancellationToken cancellationToken)
- => ReadStreamAsync(ProjectKey.ToProjectKey(project), name, checksum, cancellationToken);
+ => ReadStreamAsync(ProjectKey.ToProjectKey(project), project, name, checksum, cancellationToken);
public Task ReadStreamAsync(Document document, string name, Checksum? checksum, CancellationToken cancellationToken)
- => ReadStreamAsync(DocumentKey.ToDocumentKey(document), name, checksum, cancellationToken);
+ => ReadStreamAsync(DocumentKey.ToDocumentKey(document), document, name, checksum, cancellationToken);
public Task ReadStreamAsync(string name, CancellationToken cancellationToken)
=> ReadStreamAsync(name, checksum: null, cancellationToken);
@@ -70,10 +88,10 @@ public Task ChecksumMatchesAsync(Document document, string name, Checksum
=> ReadStreamAsync(document, name, checksum: null, cancellationToken);
public Task WriteStreamAsync(Project project, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken)
- => WriteStreamAsync(ProjectKey.ToProjectKey(project), name, stream, checksum, cancellationToken);
+ => WriteStreamAsync(ProjectKey.ToProjectKey(project), project, name, stream, checksum, cancellationToken);
public Task WriteStreamAsync(Document document, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken)
- => WriteStreamAsync(DocumentKey.ToDocumentKey(document), name, stream, checksum, cancellationToken);
+ => WriteStreamAsync(DocumentKey.ToDocumentKey(document), document, name, stream, checksum, cancellationToken);
public Task WriteStreamAsync(string name, Stream stream, CancellationToken cancellationToken)
=> WriteStreamAsync(name, stream, checksum: null, cancellationToken);
diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/PersistentStorageServiceFactory.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/DefaultPersistentStorageServiceFactory.cs
similarity index 78%
rename from src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/PersistentStorageServiceFactory.cs
rename to src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/DefaultPersistentStorageServiceFactory.cs
index f9882b2c84b79..274c98d2c062f 100644
--- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/PersistentStorageServiceFactory.cs
+++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/DefaultPersistentStorageServiceFactory.cs
@@ -2,8 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-#nullable disable
-
using System;
using System.Composition;
using Microsoft.CodeAnalysis.Host.Mef;
@@ -15,15 +13,15 @@ namespace Microsoft.CodeAnalysis.Host
/// projects or documents across runtime sessions.
///
[ExportWorkspaceServiceFactory(typeof(IPersistentStorageService), ServiceLayer.Default), Shared]
- internal class PersistentStorageServiceFactory : IWorkspaceServiceFactory
+ internal class DefaultPersistentStorageServiceFactory : IWorkspaceServiceFactory
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
- public PersistentStorageServiceFactory()
+ public DefaultPersistentStorageServiceFactory()
{
}
public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices)
- => NoOpPersistentStorageService.Instance;
+ => NoOpPersistentStorageService.GetOrThrow(workspaceServices.Workspace.Options);
}
}
diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/DocumentKey.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/DocumentKey.cs
index 767182f21a196..0985abe979c93 100644
--- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/DocumentKey.cs
+++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/DocumentKey.cs
@@ -2,8 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-#nullable disable
-
using System.Runtime.Serialization;
using Microsoft.CodeAnalysis.Host;
@@ -20,10 +18,10 @@ internal readonly struct DocumentKey
public readonly ProjectKey Project;
public readonly DocumentId Id;
- public readonly string FilePath;
+ public readonly string? FilePath;
public readonly string Name;
- public DocumentKey(ProjectKey project, DocumentId id, string filePath, string name)
+ public DocumentKey(ProjectKey project, DocumentId id, string? filePath, string name)
{
Project = project;
Id = id;
@@ -32,7 +30,10 @@ public DocumentKey(ProjectKey project, DocumentId id, string filePath, string na
}
public static DocumentKey ToDocumentKey(Document document)
- => new(ProjectKey.ToProjectKey(document.Project), document.Id, document.FilePath, document.Name);
+ => ToDocumentKey(ProjectKey.ToProjectKey(document.Project), document.State);
+
+ public static DocumentKey ToDocumentKey(ProjectKey projectKey, TextDocumentState state)
+ => new(projectKey, state.Id, state.FilePath, state.Name);
public SerializableDocumentKey Dehydrate()
=> new(Project.Dehydrate(), Id, FilePath, Name);
@@ -48,12 +49,12 @@ internal readonly struct SerializableDocumentKey
public readonly DocumentId Id;
[DataMember(Order = 2)]
- public readonly string FilePath;
+ public readonly string? FilePath;
[DataMember(Order = 3)]
public readonly string Name;
- public SerializableDocumentKey(SerializableProjectKey project, DocumentId id, string filePath, string name)
+ public SerializableDocumentKey(SerializableProjectKey project, DocumentId id, string? filePath, string name)
{
Project = project;
Id = id;
diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IChecksummedPersistentStorage.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IChecksummedPersistentStorage.cs
index b418ca5a3890a..8feb549367738 100644
--- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IChecksummedPersistentStorage.cs
+++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IChecksummedPersistentStorage.cs
@@ -60,27 +60,48 @@ internal interface IChecksummedPersistentStorage : IPersistentStorage
Task ReadStreamAsync(DocumentKey document, string name, Checksum checksum = null, CancellationToken cancellationToken = default);
///
- /// Reads the stream for the solution with the given . An optional
- /// can be provided to store along with the data. This can be used along with ReadStreamAsync with future
- /// reads to ensure the data is only read back if it matches that checksum.
+ /// Reads the stream for the solution with the given . An optional can be provided to store along with the data. This can be used along with ReadStreamAsync
+ /// with future reads to ensure the data is only read back if it matches that checksum.
+ ///
+ /// Returns if the data was successfully persisted to the storage subsystem. Subsequent
+ /// calls to read the same keys should succeed if called within the same session.
+ ///
///
Task WriteStreamAsync(string name, Stream stream, Checksum checksum = null, CancellationToken cancellationToken = default);
///
- /// Reads the stream for the with the given . An optional
- /// can be provided to store along with the data. This can be used along with ReadStreamAsync with future
- /// reads to ensure the data is only read back if it matches that checksum.
+ /// Reads the stream for the with the given . An optional
+ /// can be provided to store along with the data. This can be used along with
+ /// ReadStreamAsync with future reads to ensure the data is only read back if it matches that checksum.
+ ///
+ /// Returns if the data was successfully persisted to the storage subsystem. Subsequent
+ /// calls to read the same keys should succeed if called within the same session.
+ ///
///
Task WriteStreamAsync(Project project, string name, Stream stream, Checksum checksum = null, CancellationToken cancellationToken = default);
///
- /// Reads the stream for the with the given . An optional
- /// can be provided to store along with the data. This can be used along with ReadStreamAsync with future
- /// reads to ensure the data is only read back if it matches that checksum.
+ /// Reads the stream for the with the given . An optional
+ /// can be provided to store along with the data. This can be used along with
+ /// ReadStreamAsync with future reads to ensure the data is only read back if it matches that checksum.
+ ///
+ /// Returns if the data was successfully persisted to the storage subsystem. Subsequent
+ /// calls to read the same keys should succeed if called within the same session.
+ ///
///
Task WriteStreamAsync(Document document, string name, Stream stream, Checksum checksum = null, CancellationToken cancellationToken = default);
+ ///
+ /// Returns if the data was successfully persisted to the storage subsystem. Subsequent
+ /// calls to read the same keys should succeed if called within the same session.
+ ///
Task WriteStreamAsync(ProjectKey projectKey, string name, Stream stream, Checksum checksum = null, CancellationToken cancellationToken = default);
+
+ ///
+ /// Returns if the data was successfully persisted to the storage subsystem. Subsequent
+ /// calls to read the same keys should succeed if called within the same session.
+ ///
Task WriteStreamAsync(DocumentKey documentKey, string name, Stream stream, Checksum checksum = null, CancellationToken cancellationToken = default);
}
}
diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IPersistentStorage.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IPersistentStorage.cs
index c15c8d1601937..a602e12aceedd 100644
--- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IPersistentStorage.cs
+++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IPersistentStorage.cs
@@ -20,8 +20,22 @@ public interface IPersistentStorage : IDisposable, IAsyncDisposable
Task ReadStreamAsync(Project project, string name, CancellationToken cancellationToken = default);
Task ReadStreamAsync(Document document, string name, CancellationToken cancellationToken = default);
+ ///
+ /// Returns if the data was successfully persisted to the storage subsystem. Subsequent
+ /// calls to read the same keys should succeed if called within the same session.
+ ///
Task WriteStreamAsync(string name, Stream stream, CancellationToken cancellationToken = default);
+
+ ///
+ /// Returns if the data was successfully persisted to the storage subsystem. Subsequent
+ /// calls to read the same keys should succeed if called within the same session.
+ ///
Task WriteStreamAsync(Project project, string name, Stream stream, CancellationToken cancellationToken = default);
+
+ ///
+ /// Returns if the data was successfully persisted to the storage subsystem. Subsequent
+ /// calls to read the same keys should succeed if called within the same session.
+ ///
Task WriteStreamAsync(Document document, string name, Stream stream, CancellationToken cancellationToken = default);
}
}
diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorage.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorage.cs
index d2bf20ad513a6..0f2784a222134 100644
--- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorage.cs
+++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorage.cs
@@ -2,22 +2,30 @@
// 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.Options;
using Microsoft.CodeAnalysis.PersistentStorage;
+using Microsoft.CodeAnalysis.Storage;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Host
{
internal class NoOpPersistentStorage : IChecksummedPersistentStorage
{
- public static readonly IChecksummedPersistentStorage Instance = new NoOpPersistentStorage();
+ private static readonly IChecksummedPersistentStorage Instance = new NoOpPersistentStorage();
private NoOpPersistentStorage()
{
}
+ public static IChecksummedPersistentStorage GetOrThrow(OptionSet optionSet)
+ => optionSet.GetOption(StorageOptions.DatabaseMustSucceed)
+ ? throw new InvalidOperationException("Database was not supported")
+ : Instance;
+
public void Dispose()
{
}
@@ -89,5 +97,10 @@ public Task WriteStreamAsync(ProjectKey projectKey, string name, Stream st
public Task WriteStreamAsync(DocumentKey documentKey, string name, Stream stream, Checksum checksum, CancellationToken cancellationToken)
=> SpecializedTasks.False;
+
+ public struct TestAccessor
+ {
+ public static readonly IChecksummedPersistentStorage StorageInstance = Instance;
+ }
}
}
diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorageService.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorageService.cs
index 4f00907cfeb2e..404d7cd1dccf6 100644
--- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorageService.cs
+++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorageService.cs
@@ -2,33 +2,45 @@
// 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.Threading;
using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PersistentStorage;
+using Microsoft.CodeAnalysis.Storage;
namespace Microsoft.CodeAnalysis.Host
{
internal class NoOpPersistentStorageService : IChecksummedPersistentStorageService
{
+#if DOTNET_BUILD_FROM_SOURCE
public static readonly IPersistentStorageService Instance = new NoOpPersistentStorageService();
+#else
+ private static readonly IPersistentStorageService Instance = new NoOpPersistentStorageService();
+#endif
private NoOpPersistentStorageService()
{
}
+ public static IPersistentStorageService GetOrThrow(OptionSet optionSet)
+ => optionSet.GetOption(StorageOptions.DatabaseMustSucceed)
+ ? throw new InvalidOperationException("Database was not supported")
+ : Instance;
+
public IPersistentStorage GetStorage(Solution solution)
- => NoOpPersistentStorage.Instance;
+ => NoOpPersistentStorage.GetOrThrow(solution.Options);
public ValueTask GetStorageAsync(Solution solution, CancellationToken cancellationToken)
- => new(NoOpPersistentStorage.Instance);
+ => new(NoOpPersistentStorage.GetOrThrow(solution.Options));
ValueTask IChecksummedPersistentStorageService.GetStorageAsync(Solution solution, CancellationToken cancellationToken)
- => new(NoOpPersistentStorage.Instance);
+ => new(NoOpPersistentStorage.GetOrThrow(solution.Options));
ValueTask IChecksummedPersistentStorageService.GetStorageAsync(Solution solution, bool checkBranchId, CancellationToken cancellationToken)
- => new(NoOpPersistentStorage.Instance);
+ => new(NoOpPersistentStorage.GetOrThrow(solution.Options));
ValueTask IChecksummedPersistentStorageService.GetStorageAsync(Workspace workspace, SolutionKey solutionKey, bool checkBranchId, CancellationToken cancellationToken)
- => new(NoOpPersistentStorage.Instance);
+ => new(NoOpPersistentStorage.GetOrThrow(workspace.Options));
}
}
diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/ProjectKey.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/ProjectKey.cs
index 4b04854054ce9..895b901b442f9 100644
--- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/ProjectKey.cs
+++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/ProjectKey.cs
@@ -2,8 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-#nullable disable
-
using System.Runtime.Serialization;
using Microsoft.CodeAnalysis.Host;
@@ -20,11 +18,11 @@ internal readonly struct ProjectKey
public readonly SolutionKey Solution;
public readonly ProjectId Id;
- public readonly string FilePath;
+ public readonly string? FilePath;
public readonly string Name;
public readonly Checksum ParseOptionsChecksum;
- public ProjectKey(SolutionKey solution, ProjectId id, string filePath, string name, Checksum parseOptionsChecksum)
+ public ProjectKey(SolutionKey solution, ProjectId id, string? filePath, string name, Checksum parseOptionsChecksum)
{
Solution = solution;
Id = id;
@@ -37,7 +35,10 @@ public static ProjectKey ToProjectKey(Project project)
=> ToProjectKey(project.Solution.State, project.State);
public static ProjectKey ToProjectKey(SolutionState solutionState, ProjectState projectState)
- => new(SolutionKey.ToSolutionKey(solutionState), projectState.Id, projectState.FilePath, projectState.Name, projectState.GetParseOptionsChecksum());
+ => ToProjectKey(SolutionKey.ToSolutionKey(solutionState), projectState);
+
+ public static ProjectKey ToProjectKey(SolutionKey solutionKey, ProjectState projectState)
+ => new(solutionKey, projectState.Id, projectState.FilePath, projectState.Name, projectState.GetParseOptionsChecksum());
public SerializableProjectKey Dehydrate()
=> new(Solution.Dehydrate(), Id, FilePath, Name, ParseOptionsChecksum);
@@ -53,7 +54,7 @@ internal readonly struct SerializableProjectKey
public readonly ProjectId Id;
[DataMember(Order = 2)]
- public readonly string FilePath;
+ public readonly string? FilePath;
[DataMember(Order = 3)]
public readonly string Name;
@@ -61,7 +62,7 @@ internal readonly struct SerializableProjectKey
[DataMember(Order = 4)]
public readonly Checksum ParseOptionsChecksum;
- public SerializableProjectKey(SerializableSolutionKey solution, ProjectId id, string filePath, string name, Checksum parseOptionsChecksum)
+ public SerializableProjectKey(SerializableSolutionKey solution, ProjectId id, string? filePath, string name, Checksum parseOptionsChecksum)
{
Solution = solution;
Id = id;
diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/SolutionKey.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/SolutionKey.cs
index 2b457d1b73c8b..ae17981a4ca1a 100644
--- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/SolutionKey.cs
+++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/SolutionKey.cs
@@ -2,8 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-#nullable disable
-
using System.Runtime.Serialization;
using Microsoft.CodeAnalysis.Host;
@@ -18,10 +16,10 @@ namespace Microsoft.CodeAnalysis.PersistentStorage
internal readonly struct SolutionKey
{
public readonly SolutionId Id;
- public readonly string FilePath;
+ public readonly string? FilePath;
public readonly bool IsPrimaryBranch;
- public SolutionKey(SolutionId id, string filePath, bool isPrimaryBranch)
+ public SolutionKey(SolutionId id, string? filePath, bool isPrimaryBranch)
{
Id = id;
FilePath = filePath;
@@ -45,12 +43,12 @@ internal readonly struct SerializableSolutionKey
public readonly SolutionId Id;
[DataMember(Order = 1)]
- public readonly string FilePath;
+ public readonly string? FilePath;
[DataMember(Order = 2)]
public readonly bool IsPrimaryBranch;
- public SerializableSolutionKey(SolutionId id, string filePath, bool isPrimaryBranch)
+ public SerializableSolutionKey(SolutionId id, string? filePath, bool isPrimaryBranch)
{
Id = id;
FilePath = filePath;
diff --git a/src/Workspaces/CoreTest/Host/WorkspaceServices/TestPersistenceService.cs b/src/Workspaces/CoreTest/Host/WorkspaceServices/TestPersistenceService.cs
index 5d4971387b802..c5b92c0c957f1 100644
--- a/src/Workspaces/CoreTest/Host/WorkspaceServices/TestPersistenceService.cs
+++ b/src/Workspaces/CoreTest/Host/WorkspaceServices/TestPersistenceService.cs
@@ -21,9 +21,9 @@ public TestPersistenceService()
}
public IPersistentStorage GetStorage(Solution solution)
- => NoOpPersistentStorage.Instance;
+ => NoOpPersistentStorage.GetOrThrow(solution.Options);
public ValueTask GetStorageAsync(Solution solution, CancellationToken cancellationToken)
- => new(NoOpPersistentStorage.Instance);
+ => new(NoOpPersistentStorage.GetOrThrow(solution.Options));
}
}
diff --git a/src/Workspaces/Remote/ServiceHub/Host/IGlobalServiceBroker.cs b/src/Workspaces/Remote/ServiceHub/Host/IGlobalServiceBroker.cs
new file mode 100644
index 0000000000000..d75fc44e7567b
--- /dev/null
+++ b/src/Workspaces/Remote/ServiceHub/Host/IGlobalServiceBroker.cs
@@ -0,0 +1,50 @@
+// 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.Composition;
+using System.Threading;
+using Microsoft.CodeAnalysis.Host.Mef;
+using Microsoft.ServiceHub.Framework;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.Remote.Host
+{
+ internal interface IGlobalServiceBroker
+ {
+ IServiceBroker Instance { get; }
+ }
+
+ ///
+ /// Hacky way to expose a to workspace services that expect there to be a global
+ /// singleton (like in visual studio). Effectively the first service that gets called into will record its
+ /// broker here for these services to use.
+ ///
+ // Note: this Export is only so MEF picks up the exported member internally.
+ [Export(typeof(IGlobalServiceBroker)), Shared]
+ internal class GlobalServiceBroker : IGlobalServiceBroker
+ {
+ private static IServiceBroker? s_instance;
+
+ [ImportingConstructor]
+ [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
+ public GlobalServiceBroker()
+ {
+ }
+
+ public static void RegisterServiceBroker(IServiceBroker serviceBroker)
+ {
+ Interlocked.CompareExchange(ref s_instance, serviceBroker, null);
+ }
+
+ public IServiceBroker Instance
+ {
+ get
+ {
+ Contract.ThrowIfNull(s_instance, "Global service broker not registered");
+ return s_instance;
+ }
+ }
+ }
+}
diff --git a/src/Workspaces/Remote/ServiceHub/Host/Storage/RemoteCloudCacheStorageServiceFactory.cs b/src/Workspaces/Remote/ServiceHub/Host/Storage/RemoteCloudCacheStorageServiceFactory.cs
new file mode 100644
index 0000000000000..8b0ae8a90cebe
--- /dev/null
+++ b/src/Workspaces/Remote/ServiceHub/Host/Storage/RemoteCloudCacheStorageServiceFactory.cs
@@ -0,0 +1,72 @@
+// 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.Composition;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Host;
+using Microsoft.CodeAnalysis.Host.Mef;
+using Microsoft.CodeAnalysis.Remote.Host;
+using Microsoft.CodeAnalysis.Storage;
+using Microsoft.CodeAnalysis.Storage.CloudCache;
+using Microsoft.VisualStudio;
+using Microsoft.VisualStudio.LanguageServices.Storage;
+using Microsoft.VisualStudio.RpcContracts.Caching;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.Remote.Storage
+{
+ [ExportWorkspaceService(typeof(ICloudCacheStorageServiceFactory), WorkspaceKind.RemoteWorkspace), Shared]
+ internal class RemoteCloudCacheStorageServiceFactory : ICloudCacheStorageServiceFactory
+ {
+ private readonly IGlobalServiceBroker _globalServiceBroker;
+
+ [ImportingConstructor]
+ [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
+ public RemoteCloudCacheStorageServiceFactory(IGlobalServiceBroker globalServiceBroker)
+ {
+ _globalServiceBroker = globalServiceBroker;
+ }
+
+ public AbstractPersistentStorageService Create(IPersistentStorageLocationService locationService)
+ => new RemoteCloudCachePersistentStorageService(_globalServiceBroker, locationService);
+
+ private class RemoteCloudCachePersistentStorageService : AbstractCloudCachePersistentStorageService
+ {
+ private readonly IGlobalServiceBroker _globalServiceBroker;
+
+ public RemoteCloudCachePersistentStorageService(IGlobalServiceBroker globalServiceBroker, IPersistentStorageLocationService locationService)
+ : base(locationService)
+ {
+ _globalServiceBroker = globalServiceBroker;
+ }
+
+ protected override void DisposeCacheService(ICacheService cacheService)
+ {
+ if (cacheService is IAsyncDisposable asyncDisposable)
+ {
+ asyncDisposable.DisposeAsync().AsTask().Wait();
+ }
+ else if (cacheService is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ }
+
+ protected override async ValueTask CreateCacheServiceAsync(CancellationToken cancellationToken)
+ {
+ var serviceBroker = _globalServiceBroker.Instance;
+
+#pragma warning disable ISB001 // Dispose of proxies
+ // cache service will be disposed inside RemoteCloudCacheService.Dispose
+ var cacheService = await serviceBroker.GetProxyAsync(VisualStudioServices.VS2019_10.CacheService, cancellationToken: cancellationToken).ConfigureAwait(false);
+#pragma warning restore ISB001 // Dispose of proxies
+
+ Contract.ThrowIfNull(cacheService);
+ return cacheService;
+ }
+ }
+ }
+}
diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemotePersistentStorageLocationService.cs b/src/Workspaces/Remote/ServiceHub/Host/Storage/RemotePersistentStorageLocationService.cs
similarity index 100%
rename from src/Workspaces/Remote/ServiceHub/Host/RemotePersistentStorageLocationService.cs
rename to src/Workspaces/Remote/ServiceHub/Host/Storage/RemotePersistentStorageLocationService.cs
diff --git a/src/Workspaces/Remote/ServiceHub/Microsoft.CodeAnalysis.Remote.ServiceHub.csproj b/src/Workspaces/Remote/ServiceHub/Microsoft.CodeAnalysis.Remote.ServiceHub.csproj
index 5e485c1cd4708..41c3b92b27149 100644
--- a/src/Workspaces/Remote/ServiceHub/Microsoft.CodeAnalysis.Remote.ServiceHub.csproj
+++ b/src/Workspaces/Remote/ServiceHub/Microsoft.CodeAnalysis.Remote.ServiceHub.csproj
@@ -28,6 +28,7 @@
+
@@ -36,6 +37,9 @@
+
+
+
diff --git a/src/Workspaces/Remote/ServiceHub/Services/BrokeredServiceBase.FactoryBase.cs b/src/Workspaces/Remote/ServiceHub/Services/BrokeredServiceBase.FactoryBase.cs
index e6abd6cb72e64..8829144873748 100644
--- a/src/Workspaces/Remote/ServiceHub/Services/BrokeredServiceBase.FactoryBase.cs
+++ b/src/Workspaces/Remote/ServiceHub/Services/BrokeredServiceBase.FactoryBase.cs
@@ -8,6 +8,7 @@
using System.IO.Pipelines;
using System.Runtime;
using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Remote.Host;
using Microsoft.ServiceHub.Framework;
using Microsoft.ServiceHub.Framework.Services;
using Nerdbank.Streams;
@@ -68,6 +69,10 @@ internal TService Create(
ServiceActivationOptions serviceActivationOptions,
IServiceBroker serviceBroker)
{
+ // Register this service broker globally (if it's the first we encounter) so it can be used by other
+ // global services that need it.
+ GlobalServiceBroker.RegisterServiceBroker(serviceBroker);
+
var descriptor = ServiceDescriptors.Instance.GetServiceDescriptorForServiceFactory(typeof(TService));
var serviceHubTraceSource = (TraceSource)hostProvidedServices.GetService(typeof(TraceSource));
var serverConnection = descriptor.WithTraceSource(serviceHubTraceSource).ConstructRpcConnection(pipe);