// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Threading; using AutoMapper; using osu.Framework.Platform; using osu.Framework.Statistics; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.IO; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Skinning; using Realms; namespace osu.Game.Database { public class RealmContextFactory : IRealmFactory { private readonly Storage storage; private readonly Scheduler scheduler; private const string database_name = @"client"; private ThreadLocal threadContexts; private readonly object writeLock = new object(); private ThreadLocal refreshCompleted = new ThreadLocal(); private bool rollbackRequired; private int currentWriteUsages; private Transaction currentWriteTransaction; public RealmContextFactory(Storage storage, Scheduler scheduler) { this.storage = storage; this.scheduler = scheduler; recreateThreadContexts(); using (CreateContext()) { // ensure our schema is up-to-date and migrated. } } private void onMigration(Migration migration, ulong oldschemaversion) { } private static readonly GlobalStatistic reads = GlobalStatistics.Get("Realm", "Get (Read)"); private static readonly GlobalStatistic writes = GlobalStatistics.Get("Realm", "Get (Write)"); private static readonly GlobalStatistic commits = GlobalStatistics.Get("Realm", "Commits"); private static readonly GlobalStatistic rollbacks = GlobalStatistics.Get("Realm", "Rollbacks"); private static readonly GlobalStatistic contexts = GlobalStatistics.Get("Realm", "Contexts"); /// /// 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 Realm Get() { reads.Value++; return getContextForCurrentThread(); } /// /// 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. /// /// A usage containing a usable context. public RealmWriteUsage GetForWrite() { writes.Value++; Monitor.Enter(writeLock); Realm context; try { context = getContextForCurrentThread(); currentWriteTransaction ??= context.BeginWrite(); } 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 }; } // TODO: remove if not necessary. public void Schedule(Action action) => scheduler.Add(action); 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(); } } 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(CreateContext, true); } protected virtual Realm CreateContext() { contexts.Value++; return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) { SchemaVersion = 5, MigrationCallback = onMigration }); } public void ResetDatabase() { lock (writeLock) { recreateThreadContexts(); storage.DeleteDatabase(database_name); } } } public static class RealmExtensions { private static readonly IMapper mapper = new MapperConfiguration(c => { c.ShouldMapField = fi => false; c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic; c.CreateMap(); c.CreateMap(); c.CreateMap(); c.CreateMap(); c.CreateMap() .ForMember(s => s.Beatmaps, d => d.MapFrom(s => s.Beatmaps)) .ForMember(s => s.Files, d => d.MapFrom(s => s.Files)) .MaxDepth(2); c.CreateMap(); c.CreateMap(); c.CreateMap(); c.CreateMap(); c.CreateMap(); c.CreateMap(); c.CreateMap(); }).CreateMapper(); public static T Detach(this T obj) where T : RealmObject { if (!obj.IsManaged) return obj; var detached = mapper.Map(obj); //typeof(RealmObject).GetField("_realm", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.SetValue(detached, null); return detached; } public static Live Wrap(this T obj, IRealmFactory contextFactory) where T : RealmObject, IHasGuidPrimaryKey => new Live(obj, contextFactory); public static Live WrapAsUnmanaged(this T obj) where T : RealmObject, IHasGuidPrimaryKey => new Live(obj, null); } }