2019-01-24 16:43:03 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
2018-04-13 17:19:50 +08:00
2022-01-18 13:19:25 +08:00
using System.IO ;
2018-05-28 18:56:27 +08:00
using System.Linq ;
2018-04-13 17:19:50 +08:00
using System.Threading ;
2018-05-28 18:56:27 +08:00
using Microsoft.EntityFrameworkCore.Storage ;
2022-01-18 13:41:02 +08:00
using osu.Framework.Logging ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Platform ;
2019-07-02 12:40:40 +08:00
using osu.Framework.Statistics ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Database
{
public class DatabaseContextFactory : IDatabaseContextFactory
{
2018-07-18 15:43:46 +08:00
private readonly Storage storage ;
2018-04-13 17:19:50 +08:00
2022-01-11 21:16:06 +08:00
public const string DATABASE_NAME = @"client.db" ;
2018-04-13 17:19:50 +08:00
private ThreadLocal < OsuDbContext > threadContexts ;
private readonly object writeLock = new object ( ) ;
private bool currentWriteDidWrite ;
2018-05-28 18:56:27 +08:00
private bool currentWriteDidError ;
2018-04-13 17:19:50 +08:00
private int currentWriteUsages ;
2018-05-28 18:56:27 +08:00
private IDbContextTransaction currentWriteTransaction ;
2018-07-18 15:43:46 +08:00
public DatabaseContextFactory ( Storage storage )
2018-04-13 17:19:50 +08:00
{
2018-07-18 15:43:46 +08:00
this . storage = storage ;
2018-04-13 17:19:50 +08:00
recycleThreadContexts ( ) ;
}
2019-07-02 12:40:40 +08:00
private static readonly GlobalStatistic < int > reads = GlobalStatistics . Get < int > ( "Database" , "Get (Read)" ) ;
private static readonly GlobalStatistic < int > writes = GlobalStatistics . Get < int > ( "Database" , "Get (Write)" ) ;
private static readonly GlobalStatistic < int > commits = GlobalStatistics . Get < int > ( "Database" , "Commits" ) ;
private static readonly GlobalStatistic < int > rollbacks = GlobalStatistics . Get < int > ( "Database" , "Rollbacks" ) ;
2018-04-13 17:19:50 +08:00
/// <summary>
/// Get a context for the current thread for read-only usage.
/// If a <see cref="DatabaseWriteUsage"/> is in progress, the existing write-safe context will be returned.
/// </summary>
2019-07-02 12:40:40 +08:00
public OsuDbContext Get ( )
{
reads . Value + + ;
return threadContexts . Value ;
}
2018-04-13 17:19:50 +08:00
/// <summary>
/// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context).
/// This method may block if a write is already active on a different thread.
/// </summary>
2018-05-29 09:59:39 +08:00
/// <param name="withTransaction">Whether to start a transaction for this write.</param>
2018-04-13 17:19:50 +08:00
/// <returns>A usage containing a usable context.</returns>
2018-05-29 09:59:39 +08:00
public DatabaseWriteUsage GetForWrite ( bool withTransaction = true )
2018-04-13 17:19:50 +08:00
{
2019-07-02 12:40:40 +08:00
writes . Value + + ;
2018-04-13 17:19:50 +08:00
Monitor . Enter ( writeLock ) ;
2018-06-04 01:07:02 +08:00
OsuDbContext context ;
2018-04-13 17:19:50 +08:00
2018-06-04 01:07:02 +08:00
try
2018-05-30 12:43:25 +08:00
{
2018-06-04 01:07:02 +08:00
if ( currentWriteTransaction = = null & & withTransaction )
{
// this mitigates the fact that changes on tracked entities will not be rolled back with the transaction by ensuring write operations are always executed in isolated contexts.
// if this results in sub-optimal efficiency, we may need to look into removing Database-level transactions in favour of running SaveChanges where we currently commit the transaction.
if ( threadContexts . IsValueCreated )
recycleThreadContexts ( ) ;
2018-05-30 12:43:25 +08:00
2018-06-04 01:07:02 +08:00
context = threadContexts . Value ;
currentWriteTransaction = context . Database . BeginTransaction ( ) ;
}
else
{
2018-06-06 21:05:25 +08:00
// we want to try-catch the retrieval of the context because it could throw an error (in CreateContext).
2018-06-04 01:07:02 +08:00
context = threadContexts . Value ;
}
}
2019-04-25 16:36:17 +08:00
catch
2018-06-04 01:07:02 +08:00
{
// retrieval of a context could trigger a fatal error.
Monitor . Exit ( writeLock ) ;
throw ;
2018-05-30 12:43:25 +08:00
}
2018-05-28 18:56:27 +08:00
2018-04-13 17:19:50 +08:00
Interlocked . Increment ( ref currentWriteUsages ) ;
2018-06-04 01:07:02 +08:00
return new DatabaseWriteUsage ( context , usageCompleted ) { IsTransactionLeader = currentWriteTransaction ! = null & & currentWriteUsages = = 1 } ;
2018-04-13 17:19:50 +08:00
}
private void usageCompleted ( DatabaseWriteUsage usage )
{
int usages = Interlocked . Decrement ( ref currentWriteUsages ) ;
try
{
currentWriteDidWrite | = usage . PerformedWrite ;
2018-05-28 18:56:27 +08:00
currentWriteDidError | = usage . Errors . Any ( ) ;
2018-04-13 17:19:50 +08:00
2018-05-30 12:37:52 +08:00
if ( usages = = 0 )
2018-04-13 17:19:50 +08:00
{
2018-05-30 12:37:52 +08:00
if ( currentWriteDidError )
2019-07-02 12:40:40 +08:00
{
rollbacks . Value + + ;
2018-05-30 12:37:52 +08:00
currentWriteTransaction ? . Rollback ( ) ;
2019-07-02 12:40:40 +08:00
}
2018-05-30 12:37:52 +08:00
else
2019-07-02 12:40:40 +08:00
{
commits . Value + + ;
2018-05-30 12:37:52 +08:00
currentWriteTransaction ? . Commit ( ) ;
2019-07-02 12:40:40 +08:00
}
2018-05-30 12:37:52 +08:00
if ( currentWriteDidWrite | | currentWriteDidError )
{
// explicitly dispose to ensure any outstanding flushes happen as soon as possible (and underlying resources are purged).
usage . Context . Dispose ( ) ;
// once all writes are complete, we want to refresh thread-specific contexts to make sure they don't have stale local caches.
recycleThreadContexts ( ) ;
}
currentWriteTransaction = null ;
currentWriteDidWrite = false ;
currentWriteDidError = false ;
2018-04-13 17:19:50 +08:00
}
}
finally
{
Monitor . Exit ( writeLock ) ;
}
}
2018-07-24 18:11:14 +08:00
private void recycleThreadContexts ( )
{
2018-08-22 13:07:52 +08:00
// Contexts for other threads are not disposed as they may be in use elsewhere. Instead, fresh contexts are exposed
// for other threads to use, and we rely on the finalizer inside OsuDbContext to handle their previous contexts
threadContexts ? . Value . Dispose ( ) ;
2018-07-24 18:11:14 +08:00
threadContexts = new ThreadLocal < OsuDbContext > ( CreateContext , true ) ;
}
2018-04-13 17:19:50 +08:00
2022-01-11 21:16:06 +08:00
protected virtual OsuDbContext CreateContext ( ) = > new OsuDbContext ( CreateDatabaseConnectionString ( DATABASE_NAME , storage ) )
2018-04-13 17:19:50 +08:00
{
2018-06-04 01:07:02 +08:00
Database = { AutoTransactionsEnabled = false }
} ;
2018-04-13 17:19:50 +08:00
2022-01-18 13:19:25 +08:00
public void CreateBackup ( string filename )
{
2022-01-18 13:41:02 +08:00
Logger . Log ( $"Creating full EF database backup at {filename}" , LoggingTarget . Database ) ;
2022-01-18 13:19:25 +08:00
using ( var source = storage . GetStream ( DATABASE_NAME ) )
using ( var destination = storage . GetStream ( filename , FileAccess . Write , FileMode . CreateNew ) )
source . CopyTo ( destination ) ;
}
2018-04-13 17:19:50 +08:00
public void ResetDatabase ( )
{
lock ( writeLock )
{
recycleThreadContexts ( ) ;
2019-12-12 13:04:57 +08:00
try
{
2022-01-11 21:16:06 +08:00
storage . Delete ( DATABASE_NAME ) ;
2019-12-12 13:04:57 +08:00
}
catch
{
// for now we are not sure why file handles are kept open by EF, but this is generally only used in testing
}
2018-04-13 17:19:50 +08:00
}
}
2020-05-11 20:37:07 +08:00
public void FlushConnections ( )
{
2021-09-16 21:48:09 +08:00
if ( threadContexts ! = null )
{
foreach ( var context in threadContexts . Values )
context . Dispose ( ) ;
}
2020-05-11 20:37:07 +08:00
recycleThreadContexts ( ) ;
}
2021-09-27 16:32:40 +08:00
public static string CreateDatabaseConnectionString ( string filename , Storage storage ) = > string . Concat ( "Data Source=" , storage . GetFullPath ( $@"{filename}" , true ) ) ;
2018-04-13 17:19:50 +08:00
}
}