2019-01-24 08:43:03 +00: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 09:19:50 +00:00
2018-05-28 10:56:27 +00:00
using System.Linq ;
2018-04-13 09:19:50 +00:00
using System.Threading ;
2018-05-28 10:56:27 +00:00
using Microsoft.EntityFrameworkCore.Storage ;
2018-04-13 09:19:50 +00:00
using osu.Framework.Platform ;
2019-07-02 04:40:40 +00:00
using osu.Framework.Statistics ;
2018-04-13 09:19:50 +00:00
namespace osu.Game.Database
{
public class DatabaseContextFactory : IDatabaseContextFactory
{
2018-07-18 07:43:46 +00:00
private readonly Storage storage ;
2018-04-13 09:19:50 +00:00
2021-09-27 08:32:40 +00:00
private const string database_name = @"client.db" ;
2018-04-13 09:19:50 +00:00
private ThreadLocal < OsuDbContext > threadContexts ;
private readonly object writeLock = new object ( ) ;
private bool currentWriteDidWrite ;
2018-05-28 10:56:27 +00:00
private bool currentWriteDidError ;
2018-04-13 09:19:50 +00:00
private int currentWriteUsages ;
2018-05-28 10:56:27 +00:00
private IDbContextTransaction currentWriteTransaction ;
2018-07-18 07:43:46 +00:00
public DatabaseContextFactory ( Storage storage )
2018-04-13 09:19:50 +00:00
{
2018-07-18 07:43:46 +00:00
this . storage = storage ;
2018-04-13 09:19:50 +00:00
recycleThreadContexts ( ) ;
}
2019-07-02 04:40:40 +00: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 09:19:50 +00: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 04:40:40 +00:00
public OsuDbContext Get ( )
{
reads . Value + + ;
return threadContexts . Value ;
}
2018-04-13 09:19:50 +00: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 01:59:39 +00:00
/// <param name="withTransaction">Whether to start a transaction for this write.</param>
2018-04-13 09:19:50 +00:00
/// <returns>A usage containing a usable context.</returns>
2018-05-29 01:59:39 +00:00
public DatabaseWriteUsage GetForWrite ( bool withTransaction = true )
2018-04-13 09:19:50 +00:00
{
2019-07-02 04:40:40 +00:00
writes . Value + + ;
2018-04-13 09:19:50 +00:00
Monitor . Enter ( writeLock ) ;
2018-06-03 17:07:02 +00:00
OsuDbContext context ;
2018-04-13 09:19:50 +00:00
2018-06-03 17:07:02 +00:00
try
2018-05-30 04:43:25 +00:00
{
2018-06-03 17:07:02 +00: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 04:43:25 +00:00
2018-06-03 17:07:02 +00:00
context = threadContexts . Value ;
currentWriteTransaction = context . Database . BeginTransaction ( ) ;
}
else
{
2018-06-06 13:05:25 +00:00
// we want to try-catch the retrieval of the context because it could throw an error (in CreateContext).
2018-06-03 17:07:02 +00:00
context = threadContexts . Value ;
}
}
2019-04-25 08:36:17 +00:00
catch
2018-06-03 17:07:02 +00:00
{
// retrieval of a context could trigger a fatal error.
Monitor . Exit ( writeLock ) ;
throw ;
2018-05-30 04:43:25 +00:00
}
2018-05-28 10:56:27 +00:00
2018-04-13 09:19:50 +00:00
Interlocked . Increment ( ref currentWriteUsages ) ;
2018-06-03 17:07:02 +00:00
return new DatabaseWriteUsage ( context , usageCompleted ) { IsTransactionLeader = currentWriteTransaction ! = null & & currentWriteUsages = = 1 } ;
2018-04-13 09:19:50 +00:00
}
private void usageCompleted ( DatabaseWriteUsage usage )
{
int usages = Interlocked . Decrement ( ref currentWriteUsages ) ;
try
{
currentWriteDidWrite | = usage . PerformedWrite ;
2018-05-28 10:56:27 +00:00
currentWriteDidError | = usage . Errors . Any ( ) ;
2018-04-13 09:19:50 +00:00
2018-05-30 04:37:52 +00:00
if ( usages = = 0 )
2018-04-13 09:19:50 +00:00
{
2018-05-30 04:37:52 +00:00
if ( currentWriteDidError )
2019-07-02 04:40:40 +00:00
{
rollbacks . Value + + ;
2018-05-30 04:37:52 +00:00
currentWriteTransaction ? . Rollback ( ) ;
2019-07-02 04:40:40 +00:00
}
2018-05-30 04:37:52 +00:00
else
2019-07-02 04:40:40 +00:00
{
commits . Value + + ;
2018-05-30 04:37:52 +00:00
currentWriteTransaction ? . Commit ( ) ;
2019-07-02 04:40:40 +00:00
}
2018-05-30 04:37:52 +00: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 09:19:50 +00:00
}
}
finally
{
Monitor . Exit ( writeLock ) ;
}
}
2018-07-24 10:11:14 +00:00
private void recycleThreadContexts ( )
{
2018-08-22 05:07:52 +00: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 10:11:14 +00:00
threadContexts = new ThreadLocal < OsuDbContext > ( CreateContext , true ) ;
}
2018-04-13 09:19:50 +00:00
2021-09-27 08:32:40 +00:00
protected virtual OsuDbContext CreateContext ( ) = > new OsuDbContext ( CreateDatabaseConnectionString ( database_name , storage ) )
2018-04-13 09:19:50 +00:00
{
2018-06-03 17:07:02 +00:00
Database = { AutoTransactionsEnabled = false }
} ;
2018-04-13 09:19:50 +00:00
public void ResetDatabase ( )
{
lock ( writeLock )
{
recycleThreadContexts ( ) ;
2019-12-12 05:04:57 +00:00
try
{
2021-09-27 08:32:40 +00:00
storage . Delete ( database_name ) ;
2019-12-12 05:04:57 +00: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 09:19:50 +00:00
}
}
2020-05-11 12:37:07 +00:00
public void FlushConnections ( )
{
2021-09-16 13:48:09 +00:00
if ( threadContexts ! = null )
{
foreach ( var context in threadContexts . Values )
context . Dispose ( ) ;
}
2020-05-11 12:37:07 +00:00
recycleThreadContexts ( ) ;
}
2021-09-27 08:32:40 +00:00
public static string CreateDatabaseConnectionString ( string filename , Storage storage ) = > string . Concat ( "Data Source=" , storage . GetFullPath ( $@"{filename}" , true ) ) ;
2018-04-13 09:19:50 +00:00
}
}