DatabaseWriteUsage

This commit is contained in:
Dean Herbert
2018-02-12 17:55:11 +09:00
parent cc948d688f
commit edc3638175
14 changed files with 385 additions and 354 deletions

View File

@ -1,10 +1,8 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Platform;
@ -17,9 +15,7 @@ namespace osu.Game.Database
/// <summary>
/// Create a new <see cref="OsuDbContext"/> instance (separate from the shared context via <see cref="GetContext"/> for performing isolated operations.
/// </summary>
protected readonly Func<OsuDbContext> CreateContext;
private readonly ThreadLocal<OsuDbContext> queryContext;
protected readonly DatabaseContextFactory ContextFactory;
/// <summary>
/// Refresh an instance potentially from a different thread with a local context-tracked instance.
@ -29,33 +25,27 @@ namespace osu.Game.Database
/// <typeparam name="T">A valid EF-stored type.</typeparam>
protected virtual void Refresh<T>(ref T obj, IEnumerable<T> lookupSource = null) where T : class, IHasPrimaryKey
{
var context = GetContext();
if (context.Entry(obj).State != EntityState.Detached) return;
var id = obj.ID;
var foundObject = lookupSource?.SingleOrDefault(t => t.ID == id) ?? context.Find<T>(id);
if (foundObject != null)
using (var usage = ContextFactory.GetForWrite())
{
obj = foundObject;
context.Entry(obj).Reload();
var context = usage.Context;
if (context.Entry(obj).State != EntityState.Detached) return;
var id = obj.ID;
var foundObject = lookupSource?.SingleOrDefault(t => t.ID == id) ?? context.Find<T>(id);
if (foundObject != null)
{
obj = foundObject;
context.Entry(obj).Reload();
}
else
context.Add(obj);
}
else
context.Add(obj);
}
/// <summary>
/// Retrieve a shared context for performing lookups (or write operations on the update thread, for now).
/// </summary>
protected OsuDbContext GetContext() => queryContext.Value;
protected DatabaseBackedStore(Func<OsuDbContext> createContext, Storage storage = null)
protected DatabaseBackedStore(DatabaseContextFactory contextFactory, Storage storage = null)
{
CreateContext = createContext;
// todo: while this seems to work quite well, we need to consider that contexts could enter a state where they are never cleaned up.
queryContext = new ThreadLocal<OsuDbContext>(CreateContext);
ContextFactory = contextFactory;
Storage = storage;
}

View File

@ -1,6 +1,7 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Threading;
using osu.Framework.Platform;
namespace osu.Game.Database
@ -11,17 +12,70 @@ namespace osu.Game.Database
private const string database_name = @"client";
private ThreadLocal<OsuDbContext> threadContexts;
private readonly object writeLock = new object();
private OsuDbContext writeContext;
private volatile int currentWriteUsages;
public DatabaseContextFactory(GameHost host)
{
this.host = host;
recycleThreadContexts();
}
public OsuDbContext GetContext() => new OsuDbContext(host.Storage.GetDatabaseConnectionString(database_name));
/// <summary>
/// Get a context for read-only usage.
/// </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>
/// <returns>A usage containing a usable context.</returns>
public DatabaseWriteUsage GetForWrite()
{
lock (writeLock)
{
var usage = new DatabaseWriteUsage(writeContext ?? (writeContext = threadContexts.Value), usageCompleted);
Interlocked.Increment(ref currentWriteUsages);
return usage;
}
}
private void usageCompleted(DatabaseWriteUsage usage)
{
int usages = Interlocked.Decrement(ref currentWriteUsages);
if (usages == 0)
{
writeContext.Dispose();
writeContext = null;
// once all writes are complete, we want to refresh thread-specific contexts to make sure they don't have stale local caches.
recycleThreadContexts();
}
}
private void recycleThreadContexts() => threadContexts = new ThreadLocal<OsuDbContext>(CreateContext);
protected virtual OsuDbContext CreateContext()
{
var ctx = new OsuDbContext(host.Storage.GetDatabaseConnectionString(database_name));
ctx.Database.AutoTransactionsEnabled = false;
return ctx;
}
public void ResetDatabase()
{
// todo: we probably want to make sure there are no active contexts before performing this operation.
host.Storage.DeleteDatabase(database_name);
lock (writeLock)
{
recycleThreadContexts();
host.Storage.DeleteDatabase(database_name);
}
}
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using Microsoft.EntityFrameworkCore.Storage;
namespace osu.Game.Database
{
public class DatabaseWriteUsage : IDisposable
{
public readonly OsuDbContext Context;
private readonly IDbContextTransaction transaction;
private readonly Action<DatabaseWriteUsage> usageCompleted;
public DatabaseWriteUsage(OsuDbContext context, Action<DatabaseWriteUsage> onCompleted)
{
Context = context;
transaction = Context.BeginTransaction();
usageCompleted = onCompleted;
}
public void Dispose()
{
Context.SaveChanges(transaction);
usageCompleted?.Invoke(this);
}
}
}

View File

@ -0,0 +1,21 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Database
{
public class SingletonContextFactory : DatabaseContextFactory
{
private readonly OsuDbContext context;
public SingletonContextFactory(OsuDbContext context)
: base(null)
{
this.context = context;
}
protected override OsuDbContext CreateContext()
{
return context;
}
}
}