From bcb04f616895f166cdfff223e781d11dda77aba0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 May 2018 19:56:27 +0900 Subject: [PATCH] Improve transaction handling flexibility --- osu.Game/Database/ArchiveModelManager.cs | 66 +++++++++++++++++---- osu.Game/Database/DatabaseContextFactory.cs | 23 ++++++- osu.Game/Database/DatabaseWriteUsage.cs | 16 +++-- osu.Game/Database/OsuDbContext.cs | 13 ---- 4 files changed, 84 insertions(+), 34 deletions(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index e04559d547..28c25c873f 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -56,13 +56,42 @@ namespace osu.Game.Database // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) private ArchiveImportIPCChannel ipc; + private readonly List cachedEvents = new List(); + + private bool delayingEvents; + + private void cacheEvents() + { + delayingEvents = true; + } + + private void flushEvents(bool perform) + { + if (perform) + { + foreach (var a in cachedEvents) + a.Invoke(); + } + + cachedEvents.Clear(); + delayingEvents = false; + } + + private void handleEvent(Action a) + { + if (delayingEvents) + cachedEvents.Add(a); + else + a.Invoke(); + } + protected ArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, MutableDatabaseBackedStore modelStore, IIpcHost importHost = null) { ContextFactory = contextFactory; ModelStore = modelStore; - ModelStore.ItemAdded += s => ItemAdded?.Invoke(s); - ModelStore.ItemRemoved += s => ItemRemoved?.Invoke(s); + ModelStore.ItemAdded += s => handleEvent(() => ItemAdded?.Invoke(s)); + ModelStore.ItemRemoved += s => handleEvent(() => ItemRemoved?.Invoke(s)); Files = new FileStore(contextFactory, storage); @@ -138,24 +167,37 @@ namespace osu.Game.Database /// The archive to be imported. public TModel Import(ArchiveReader archive) { - using (ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes. + TModel item = null; + cacheEvents(); + + try { - // create a new model (don't yet add to database) - var item = CreateModel(archive); + using (var write = ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes. + { + if (!write.IsTransactionLeader) throw new InvalidOperationException($"Ensure there is no parent transaction so errors can correctly be handled by {this}"); - var existing = CheckForExisting(item); + // create a new model (don't yet add to database) + item = CreateModel(archive); - if (existing != null) return existing; + var existing = CheckForExisting(item); - item.Files = createFileInfos(archive, Files); + if (existing != null) return existing; - Populate(item, archive); + item.Files = createFileInfos(archive, Files); - // import to store - ModelStore.Add(item); + Populate(item, archive); - return item; + // import to store + ModelStore.Add(item); + } } + catch + { + item = null; + } + + flushEvents(item != null); + return item; } /// diff --git a/osu.Game/Database/DatabaseContextFactory.cs b/osu.Game/Database/DatabaseContextFactory.cs index 71960303b5..65a19e23c0 100644 --- a/osu.Game/Database/DatabaseContextFactory.cs +++ b/osu.Game/Database/DatabaseContextFactory.cs @@ -1,7 +1,9 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Linq; using System.Threading; +using Microsoft.EntityFrameworkCore.Storage; using osu.Framework.Platform; namespace osu.Game.Database @@ -17,8 +19,12 @@ namespace osu.Game.Database private readonly object writeLock = new object(); private bool currentWriteDidWrite; + private bool currentWriteDidError; + private int currentWriteUsages; + private IDbContextTransaction currentWriteTransaction; + public DatabaseContextFactory(GameHost host) { this.host = host; @@ -40,9 +46,12 @@ namespace osu.Game.Database { Monitor.Enter(writeLock); + if (currentWriteTransaction == null) + currentWriteTransaction = threadContexts.Value.Database.BeginTransaction(); + Interlocked.Increment(ref currentWriteUsages); - return new DatabaseWriteUsage(threadContexts.Value, usageCompleted); + return new DatabaseWriteUsage(threadContexts.Value, usageCompleted) { IsTransactionLeader = currentWriteUsages == 1 }; } private void usageCompleted(DatabaseWriteUsage usage) @@ -52,16 +61,24 @@ namespace osu.Game.Database try { currentWriteDidWrite |= usage.PerformedWrite; + currentWriteDidError |= usage.Errors.Any(); if (usages > 0) return; + if (currentWriteDidError) + currentWriteTransaction.Rollback(); + else + currentWriteTransaction.Commit(); + + currentWriteTransaction = null; + currentWriteDidWrite = false; + currentWriteDidError = false; + if (currentWriteDidWrite) { // explicitly dispose to ensure any outstanding flushes happen as soon as possible (and underlying resources are purged). usage.Context.Dispose(); - currentWriteDidWrite = false; - // once all writes are complete, we want to refresh thread-specific contexts to make sure they don't have stale local caches. recycleThreadContexts(); } diff --git a/osu.Game/Database/DatabaseWriteUsage.cs b/osu.Game/Database/DatabaseWriteUsage.cs index 07bebbf0c3..8216c04b22 100644 --- a/osu.Game/Database/DatabaseWriteUsage.cs +++ b/osu.Game/Database/DatabaseWriteUsage.cs @@ -2,26 +2,31 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using Microsoft.EntityFrameworkCore.Storage; +using System.Collections.Generic; namespace osu.Game.Database { public class DatabaseWriteUsage : IDisposable { public readonly OsuDbContext Context; - private readonly IDbContextTransaction transaction; private readonly Action usageCompleted; public DatabaseWriteUsage(OsuDbContext context, Action onCompleted) { Context = context; - transaction = Context.BeginTransaction(); usageCompleted = onCompleted; } public bool PerformedWrite { get; private set; } private bool isDisposed; + public List Errors = new List(); + + /// + /// Whether this write usage will commit a transaction on completion. + /// If false, there is a parent usage responsible for transaction commit. + /// + public bool IsTransactionLeader = false; protected void Dispose(bool disposing) { @@ -30,12 +35,11 @@ namespace osu.Game.Database try { - PerformedWrite |= Context.SaveChanges(transaction) > 0; + PerformedWrite |= Context.SaveChanges() > 0; } catch (Exception e) { - transaction?.Rollback(); - throw; + Errors.Add(e); } finally { diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index 06503ffc5f..0ae197d62d 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -3,7 +3,6 @@ using System; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Logging; using osu.Framework.Logging; @@ -104,18 +103,6 @@ namespace osu.Game.Database modelBuilder.Entity().HasOne(b => b.BaseDifficulty); } - public IDbContextTransaction BeginTransaction() - { - return Database.BeginTransaction(); - } - - public int SaveChanges(IDbContextTransaction transaction = null) - { - var ret = base.SaveChanges(); - if (ret > 0) transaction?.Commit(); - return ret; - } - private class OsuDbLoggerFactory : ILoggerFactory { #region Disposal