File: Storage\SQLite\v2\SQLitePersistentStorage_FlushWrites.cs
Web Access
Project: ..\..\..\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System.Threading;
using System.Threading.Tasks;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.SQLite.v2
{
    internal partial class SQLitePersistentStorage
    {
        /// <summary>
        /// A queue to batch up flush requests and ensure that we don't issue then more often than every <see
        /// cref="FlushAllDelayMS"/>.
        /// </summary>
        private readonly AsyncBatchingWorkQueue _flushQueue;
 
        private void EnqueueFlushTask()
        {
            _flushQueue.AddWork();
        }
 
        private async ValueTask FlushInMemoryDataToDiskIfNotShutdownAsync(CancellationToken cancellationToken)
        {
            // When we are asked to flush, go actually acquire the write-scheduler and perform the actual writes from
            // it. Note: this is only called max every FlushAllDelayMS.  So we don't bother trying to avoid the delegate
            // allocation here.
            await PerformWriteAsync(_flushInMemoryDataToDisk, cancellationToken).ConfigureAwait(false);
        }
 
        private Task FlushWritesOnCloseAsync()
        {
            // Issue a write task to write this all out to disk.
            //
            // Note: this only happens on close, so we don't try to avoid allocations here.
 
            return PerformWriteAsync(
                () =>
                {
                    // Perform the actual write while having exclusive access to the scheduler.
                    FlushInMemoryDataToDisk();
 
                    // Now that we've done this, definitely cancel any further work. From this point on, it is now
                    // invalid for any codepaths to try to acquire a db connection for any purpose (beyond us
                    // disposing things below).
                    //
                    // This will also ensure that if we have a bg flush task still pending, when it wakes up it will
                    // see that we're shutdown and not proceed (and importantly won't acquire a connection). Because
                    // both the bg task and us run serialized, there is no way for it to miss this token
                    // cancellation.  If it runs after us, then it sees this.  If it runs before us, then we just
                    // block until it finishes.
                    //
                    // We don't have to worry about reads/writes getting connections either.
                    // The only way we can get disposed in the first place is if every user of this storage instance
                    // has released their ref on us. In that case, it would be an error on their part to ever try to
                    // read/write after releasing us.
                    _shutdownTokenSource.Cancel();
                }, CancellationToken.None);
        }
 
        private void FlushInMemoryDataToDisk()
        {
            // We're writing.  This better always be under the exclusive scheduler.
            Contract.ThrowIfFalse(TaskScheduler.Current == _connectionPoolService.Scheduler.ExclusiveScheduler);
 
            // Don't flush from a bg task if we've been asked to shutdown.  The shutdown logic in the storage service
            // will take care of the final writes to the main db.
            if (_shutdownTokenSource.IsCancellationRequested)
                return;
 
            using var _ = _connectionPool.Target.GetPooledConnection(out var connection);
 
            var exception = connection.RunInTransaction(static state =>
            {
                state.self._solutionAccessor.FlushInMemoryDataToDisk_MustRunInTransaction(state.connection);
                state.self._projectAccessor.FlushInMemoryDataToDisk_MustRunInTransaction(state.connection);
                state.self._documentAccessor.FlushInMemoryDataToDisk_MustRunInTransaction(state.connection);
            },
            (self: this, connection),
            throwOnSqlException: false);
 
            if (exception != null)
            {
                // Some sql exception occurred (like SQLITE_FULL). These are not exceptions we can suitably recover
                // from.  In this case, transition the storage instance into being unusable. Future reads/writes will
                // get empty results.
                this.DisableStorage(exception);
            }
        }
    }
}