2018-04-13 09:19:50 +00:00
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
2018-06-03 17:07:02 +00:00
using System ;
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 ;
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
private const string database_name = @"client" ;
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 ( ) ;
}
/// <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>
public OsuDbContext Get ( ) = > threadContexts . Value ;
/// <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
{
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 ;
}
}
catch ( Exception e )
{
// 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 )
currentWriteTransaction ? . Rollback ( ) ;
else
currentWriteTransaction ? . Commit ( ) ;
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
2018-07-18 07:43:46 +00:00
protected virtual OsuDbContext CreateContext ( ) = > new OsuDbContext ( storage . GetDatabaseConnectionString ( database_name ) )
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 ( ) ;
2018-07-18 07:43:46 +00:00
storage . DeleteDatabase ( database_name ) ;
2018-04-13 09:19:50 +00:00
}
}
}
}