// Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; using System.Linq; using System.Threading; using Microsoft.EntityFrameworkCore.Storage; using osu.Framework.Platform; namespace osu.Game.Database { public class DatabaseContextFactory : IDatabaseContextFactory { private readonly Storage storage; private const string database_name = @"client"; private ThreadLocal threadContexts; private readonly object writeLock = new object(); private bool currentWriteDidWrite; private bool currentWriteDidError; private int currentWriteUsages; private IDbContextTransaction currentWriteTransaction; public DatabaseContextFactory(Storage storage) { this.storage = storage; recycleThreadContexts(); } /// /// Get a context for the current thread for read-only usage. /// If a is in progress, the existing write-safe context will be returned. /// public OsuDbContext Get() => threadContexts.Value; /// /// 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. /// /// Whether to start a transaction for this write. /// A usage containing a usable context. public DatabaseWriteUsage GetForWrite(bool withTransaction = true) { Monitor.Enter(writeLock); OsuDbContext context; try { 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(); context = threadContexts.Value; currentWriteTransaction = context.Database.BeginTransaction(); } else { // we want to try-catch the retrieval of the context because it could throw an error (in CreateContext). context = threadContexts.Value; } } catch (Exception e) { // retrieval of a context could trigger a fatal error. Monitor.Exit(writeLock); throw; } Interlocked.Increment(ref currentWriteUsages); return new DatabaseWriteUsage(context, usageCompleted) { IsTransactionLeader = currentWriteTransaction != null && currentWriteUsages == 1 }; } private void usageCompleted(DatabaseWriteUsage usage) { int usages = Interlocked.Decrement(ref currentWriteUsages); try { currentWriteDidWrite |= usage.PerformedWrite; currentWriteDidError |= usage.Errors.Any(); if (usages == 0) { 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; } } finally { Monitor.Exit(writeLock); } } private void recycleThreadContexts() => threadContexts = new ThreadLocal(CreateContext); protected virtual OsuDbContext CreateContext() => new OsuDbContext(storage.GetDatabaseConnectionString(database_name)) { Database = { AutoTransactionsEnabled = false } }; public void ResetDatabase() { lock (writeLock) { recycleThreadContexts(); GC.Collect(); GC.WaitForPendingFinalizers(); storage.DeleteDatabase(database_name); } } } }