2021-01-07 13:07:36 +08: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.
|
|
|
|
|
|
|
|
using System.Threading;
|
|
|
|
using osu.Framework.Platform;
|
|
|
|
using osu.Framework.Statistics;
|
|
|
|
using Realms;
|
|
|
|
|
|
|
|
namespace osu.Game.Database
|
|
|
|
{
|
|
|
|
public class RealmContextFactory : IRealmFactory
|
|
|
|
{
|
|
|
|
private readonly Storage storage;
|
|
|
|
|
|
|
|
private const string database_name = @"client";
|
|
|
|
|
|
|
|
private ThreadLocal<Realm> threadContexts;
|
|
|
|
|
|
|
|
private readonly object writeLock = new object();
|
|
|
|
|
|
|
|
private ThreadLocal<bool> refreshCompleted = new ThreadLocal<bool>();
|
|
|
|
|
|
|
|
private bool rollbackRequired;
|
|
|
|
|
|
|
|
private int currentWriteUsages;
|
|
|
|
|
|
|
|
private Transaction currentWriteTransaction;
|
|
|
|
|
2021-01-11 17:57:56 +08:00
|
|
|
public RealmContextFactory(Storage storage)
|
2021-01-07 13:07:36 +08:00
|
|
|
{
|
|
|
|
this.storage = storage;
|
2021-01-11 17:57:56 +08:00
|
|
|
|
2021-01-07 13:07:36 +08:00
|
|
|
recreateThreadContexts();
|
2021-01-07 14:41:29 +08:00
|
|
|
|
|
|
|
using (CreateContext())
|
|
|
|
{
|
|
|
|
// ensure our schema is up-to-date and migrated.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void onMigration(Migration migration, ulong oldschemaversion)
|
|
|
|
{
|
2021-01-07 13:07:36 +08:00
|
|
|
}
|
|
|
|
|
2021-01-07 14:01:41 +08:00
|
|
|
private static readonly GlobalStatistic<int> reads = GlobalStatistics.Get<int>("Realm", "Get (Read)");
|
|
|
|
private static readonly GlobalStatistic<int> writes = GlobalStatistics.Get<int>("Realm", "Get (Write)");
|
|
|
|
private static readonly GlobalStatistic<int> commits = GlobalStatistics.Get<int>("Realm", "Commits");
|
|
|
|
private static readonly GlobalStatistic<int> rollbacks = GlobalStatistics.Get<int>("Realm", "Rollbacks");
|
|
|
|
private static readonly GlobalStatistic<int> contexts = GlobalStatistics.Get<int>("Realm", "Contexts");
|
2021-01-07 13:07:36 +08:00
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Get a context for the current thread for read-only usage.
|
|
|
|
/// If a <see cref="RealmWriteUsage"/> is in progress, the existing write-safe context will be returned.
|
|
|
|
/// </summary>
|
|
|
|
public Realm Get()
|
|
|
|
{
|
|
|
|
reads.Value++;
|
|
|
|
return getContextForCurrentThread();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <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 RealmWriteUsage GetForWrite()
|
|
|
|
{
|
|
|
|
writes.Value++;
|
|
|
|
Monitor.Enter(writeLock);
|
|
|
|
Realm context;
|
|
|
|
|
|
|
|
try
|
|
|
|
{
|
|
|
|
context = getContextForCurrentThread();
|
|
|
|
|
2021-01-07 14:51:29 +08:00
|
|
|
currentWriteTransaction ??= context.BeginWrite();
|
2021-01-07 13:07:36 +08:00
|
|
|
}
|
|
|
|
catch
|
|
|
|
{
|
|
|
|
// retrieval of a context could trigger a fatal error.
|
|
|
|
Monitor.Exit(writeLock);
|
|
|
|
throw;
|
|
|
|
}
|
|
|
|
|
|
|
|
Interlocked.Increment(ref currentWriteUsages);
|
|
|
|
|
|
|
|
return new RealmWriteUsage(context, usageCompleted) { IsTransactionLeader = currentWriteTransaction != null && currentWriteUsages == 1 };
|
|
|
|
}
|
|
|
|
|
|
|
|
private Realm getContextForCurrentThread()
|
|
|
|
{
|
|
|
|
var context = threadContexts.Value;
|
|
|
|
if (context?.IsClosed != false)
|
|
|
|
threadContexts.Value = context = CreateContext();
|
|
|
|
|
|
|
|
if (!refreshCompleted.Value)
|
|
|
|
{
|
|
|
|
context.Refresh();
|
|
|
|
refreshCompleted.Value = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return context;
|
|
|
|
}
|
|
|
|
|
|
|
|
private void usageCompleted(RealmWriteUsage usage)
|
|
|
|
{
|
|
|
|
int usages = Interlocked.Decrement(ref currentWriteUsages);
|
|
|
|
|
|
|
|
try
|
|
|
|
{
|
|
|
|
rollbackRequired |= usage.RollbackRequired;
|
|
|
|
|
|
|
|
if (usages == 0)
|
|
|
|
{
|
|
|
|
if (rollbackRequired)
|
|
|
|
{
|
|
|
|
rollbacks.Value++;
|
|
|
|
currentWriteTransaction?.Rollback();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
commits.Value++;
|
|
|
|
currentWriteTransaction?.Commit();
|
|
|
|
}
|
|
|
|
|
|
|
|
currentWriteTransaction = null;
|
|
|
|
rollbackRequired = false;
|
|
|
|
|
|
|
|
refreshCompleted = new ThreadLocal<bool>();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
finally
|
|
|
|
{
|
|
|
|
Monitor.Exit(writeLock);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void recreateThreadContexts()
|
|
|
|
{
|
|
|
|
// 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();
|
|
|
|
threadContexts = new ThreadLocal<Realm>(CreateContext, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected virtual Realm CreateContext()
|
|
|
|
{
|
|
|
|
contexts.Value++;
|
2021-01-07 14:41:29 +08:00
|
|
|
return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true))
|
|
|
|
{
|
2021-01-08 14:49:11 +08:00
|
|
|
SchemaVersion = 5,
|
2021-01-07 14:41:29 +08:00
|
|
|
MigrationCallback = onMigration
|
|
|
|
});
|
2021-01-07 13:07:36 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
public void ResetDatabase()
|
|
|
|
{
|
|
|
|
lock (writeLock)
|
|
|
|
{
|
|
|
|
recreateThreadContexts();
|
|
|
|
storage.DeleteDatabase(database_name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|