2021-01-21 19:53:16 +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.
2021-01-14 15:13:10 +08:00
using System ;
2022-01-21 17:50:25 +08:00
using System.Collections.Generic ;
using System.Diagnostics ;
2022-01-18 13:30:41 +08:00
using System.IO ;
2021-10-18 14:35:51 +08:00
using System.Linq ;
2021-11-05 16:01:00 +08:00
using System.Reflection ;
2021-01-07 13:07:36 +08:00
using System.Threading ;
2021-01-22 16:28:47 +08:00
using osu.Framework.Allocation ;
2021-07-04 16:59:39 +08:00
using osu.Framework.Development ;
2021-10-29 10:14:22 +08:00
using osu.Framework.Input.Bindings ;
2021-01-12 13:36:35 +08:00
using osu.Framework.Logging ;
2021-01-07 13:07:36 +08:00
using osu.Framework.Platform ;
using osu.Framework.Statistics ;
2021-11-22 17:07:28 +08:00
using osu.Game.Configuration ;
2021-11-19 18:07:21 +08:00
using osu.Game.Beatmaps ;
2021-10-29 10:14:22 +08:00
using osu.Game.Input.Bindings ;
2021-10-18 14:35:51 +08:00
using osu.Game.Models ;
2021-11-29 16:59:41 +08:00
using osu.Game.Skinning ;
2021-11-25 13:12:49 +08:00
using osu.Game.Stores ;
2021-11-23 12:00:33 +08:00
using osu.Game.Rulesets ;
2022-01-13 12:40:09 +08:00
using osu.Game.Scoring ;
2021-01-07 13:07:36 +08:00
using Realms ;
2022-01-19 09:58:59 +08:00
using Realms.Exceptions ;
2021-01-07 13:07:36 +08:00
2021-09-30 22:42:40 +08:00
#nullable enable
2021-01-07 13:07:36 +08:00
namespace osu.Game.Database
{
2021-09-30 22:42:40 +08:00
/// <summary>
/// A factory which provides both the main (update thread bound) realm context and creates contexts for async usage.
/// </summary>
2022-01-21 15:38:07 +08:00
public class RealmContextFactory : IDisposable
2021-01-07 13:07:36 +08:00
{
private readonly Storage storage ;
2021-01-13 17:24:19 +08:00
2021-01-12 13:36:35 +08:00
/// <summary>
2021-09-30 22:42:40 +08:00
/// The filename of this realm.
2021-01-12 13:36:35 +08:00
/// </summary>
2021-09-30 22:42:40 +08:00
public readonly string Filename ;
2021-11-23 15:27:28 +08:00
private readonly IDatabaseContextFactory ? efContextFactory ;
2021-11-22 17:51:37 +08:00
2021-10-18 14:35:51 +08:00
/// <summary>
/// Version history:
2021-11-22 14:23:16 +08:00
/// 6 ~2021-10-18 First tracked version.
/// 7 2021-10-18 Changed OnlineID fields to non-nullable to add indexing support.
/// 8 2021-10-29 Rebind scroll adjust keys to not have control modifier.
/// 9 2021-11-04 Converted BeatmapMetadata.Author from string to RealmUser.
2021-11-22 17:07:28 +08:00
/// 10 2021-11-22 Use ShortName instead of RulesetID for ruleset settings.
2021-11-22 17:34:04 +08:00
/// 11 2021-11-22 Use ShortName instead of RulesetID for ruleset key bindings.
2021-11-24 17:45:34 +08:00
/// 12 2021-11-24 Add Status to RealmBeatmapSet.
2022-01-13 12:28:46 +08:00
/// 13 2022-01-13 Final migration of beatmaps and scores to realm (multiple new storage fields).
2021-10-18 14:35:51 +08:00
/// </summary>
2022-01-13 12:28:46 +08:00
private const int schema_version = 13 ;
2021-01-07 13:07:36 +08:00
2021-06-24 13:37:26 +08:00
/// <summary>
2021-09-30 22:42:40 +08:00
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking context creation during blocking periods.
2021-06-24 13:37:26 +08:00
/// </summary>
2021-09-30 22:42:40 +08:00
private readonly SemaphoreSlim contextCreationLock = new SemaphoreSlim ( 1 ) ;
2021-06-24 13:37:26 +08:00
2021-11-29 16:38:54 +08:00
private readonly ThreadLocal < bool > currentThreadCanCreateContexts = new ThreadLocal < bool > ( ) ;
2021-11-29 15:18:57 +08:00
2021-11-23 16:47:43 +08:00
private static readonly GlobalStatistic < int > contexts_created = GlobalStatistics . Get < int > ( @"Realm" , @"Contexts (Created)" ) ;
2021-01-22 16:28:47 +08:00
2021-10-01 02:45:00 +08:00
private readonly object contextLock = new object ( ) ;
2022-01-20 19:23:17 +08:00
2021-09-30 22:42:40 +08:00
private Realm ? context ;
2021-01-13 16:34:44 +08:00
2022-01-21 21:40:18 +08:00
public Realm Context = > ensureUpdateContext ( ) ;
private Realm ensureUpdateContext ( )
2021-01-07 13:07:36 +08:00
{
2022-01-21 21:40:18 +08:00
if ( ! ThreadSafety . IsUpdateThread )
throw new InvalidOperationException ( @ $"Use {nameof(createContext)} when performing realm operations from a non-update thread" ) ;
2021-07-04 16:59:39 +08:00
2022-01-21 21:40:18 +08:00
lock ( contextLock )
{
if ( context = = null )
2021-01-13 16:34:44 +08:00
{
2022-01-21 21:40:18 +08:00
context = createContext ( ) ;
Logger . Log ( @ $"Opened realm ""{context.Config.DatabasePath}"" at version {context.Config.SchemaVersion}" ) ;
2022-01-21 17:50:25 +08:00
2022-01-21 21:40:18 +08:00
// Resubscribe any subscriptions
foreach ( var action in subscriptionActions . Keys )
registerSubscription ( action ) ;
}
2021-10-01 02:45:00 +08:00
2022-01-21 21:40:18 +08:00
Debug . Assert ( context ! = null ) ;
2022-01-21 19:45:10 +08:00
2022-01-21 21:40:18 +08:00
// creating a context will ensure our schema is up-to-date and migrated.
return context ;
2021-01-13 16:34:44 +08:00
}
2021-01-07 14:41:29 +08:00
}
2022-01-21 17:13:21 +08:00
internal static bool CurrentThreadSubscriptionsAllowed = > current_thread_subscriptions_allowed . Value ;
private static readonly ThreadLocal < bool > current_thread_subscriptions_allowed = new ThreadLocal < bool > ( ) ;
2021-11-23 16:47:43 +08:00
/// <summary>
/// Construct a new instance of a realm context factory.
/// </summary>
/// <param name="storage">The game storage which will be used to create the realm backing file.</param>
/// <param name="filename">The filename to use for the realm backing file. A ".realm" extension will be added automatically if not specified.</param>
/// <param name="efContextFactory">An EF factory used only for migration purposes.</param>
2021-11-23 15:27:28 +08:00
public RealmContextFactory ( Storage storage , string filename , IDatabaseContextFactory ? efContextFactory = null )
2021-01-07 14:41:29 +08:00
{
2021-01-13 16:34:44 +08:00
this . storage = storage ;
2021-11-23 15:27:28 +08:00
this . efContextFactory = efContextFactory ;
2021-01-07 13:07:36 +08:00
2021-09-30 22:42:40 +08:00
Filename = filename ;
2021-01-07 13:07:36 +08:00
2021-11-23 16:47:43 +08:00
const string realm_extension = @".realm" ;
2021-01-14 14:51:19 +08:00
2021-09-30 22:42:40 +08:00
if ( ! Filename . EndsWith ( realm_extension , StringComparison . Ordinal ) )
Filename + = realm_extension ;
2021-09-30 22:45:09 +08:00
2022-01-18 15:05:12 +08:00
try
{
// This method triggers the first `CreateContext` call, which will implicitly run realm migrations and bring the schema up-to-date.
cleanupPendingDeletions ( ) ;
}
catch ( Exception e )
{
Logger . Error ( e , "Realm startup failed with unrecoverable error; starting with a fresh database. A backup of your database has been made." ) ;
CreateBackup ( $"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}" ) ;
storage . Delete ( Filename ) ;
cleanupPendingDeletions ( ) ;
}
2021-09-30 22:45:09 +08:00
}
private void cleanupPendingDeletions ( )
{
2022-01-21 15:40:20 +08:00
using ( var realm = createContext ( ) )
2021-09-30 22:45:09 +08:00
using ( var transaction = realm . BeginWrite ( ) )
{
2022-01-13 12:40:09 +08:00
var pendingDeleteScores = realm . All < ScoreInfo > ( ) . Where ( s = > s . DeletePending ) ;
foreach ( var score in pendingDeleteScores )
realm . Remove ( score ) ;
2021-11-19 18:07:21 +08:00
var pendingDeleteSets = realm . All < BeatmapSetInfo > ( ) . Where ( s = > s . DeletePending ) ;
2021-09-30 22:45:09 +08:00
2022-01-12 14:09:56 +08:00
foreach ( var beatmapSet in pendingDeleteSets )
2021-09-30 22:45:09 +08:00
{
2022-01-12 14:09:56 +08:00
foreach ( var beatmap in beatmapSet . Beatmaps )
{
// Cascade delete related scores, else they will have a null beatmap against the model's spec.
foreach ( var score in beatmap . Scores )
realm . Remove ( score ) ;
2021-09-30 22:45:09 +08:00
2022-01-12 15:17:11 +08:00
realm . Remove ( beatmap . Metadata ) ;
2022-01-12 14:09:56 +08:00
realm . Remove ( beatmap ) ;
}
realm . Remove ( beatmapSet ) ;
2021-09-30 22:45:09 +08:00
}
2021-11-29 16:59:41 +08:00
var pendingDeleteSkins = realm . All < SkinInfo > ( ) . Where ( s = > s . DeletePending ) ;
foreach ( var s in pendingDeleteSkins )
realm . Remove ( s ) ;
2021-09-30 22:45:09 +08:00
transaction . Commit ( ) ;
}
2021-11-25 13:28:27 +08:00
// clean up files after dropping any pending deletions.
// in the future we may want to only do this when the game is idle, rather than on every startup.
new RealmFileStore ( this , storage ) . Cleanup ( ) ;
2021-06-24 13:37:26 +08:00
}
2021-01-13 16:34:44 +08:00
2021-09-30 22:42:40 +08:00
/// <summary>
/// Compact this realm.
/// </summary>
/// <returns></returns>
public bool Compact ( ) = > Realm . Compact ( getConfiguration ( ) ) ;
2022-01-21 00:33:45 +08:00
/// <summary>
/// Run work on realm with a return value.
/// </summary>
/// <remarks>
/// Handles correct context management automatically.
/// </remarks>
/// <param name="action">The work to run.</param>
/// <typeparam name="T">The return type.</typeparam>
public T Run < T > ( Func < Realm , T > action )
{
if ( ThreadSafety . IsUpdateThread )
return action ( Context ) ;
2022-01-21 15:40:20 +08:00
using ( var realm = createContext ( ) )
2022-01-21 00:33:45 +08:00
return action ( realm ) ;
}
/// <summary>
/// Run work on realm.
/// </summary>
/// <remarks>
/// Handles correct context management automatically.
/// </remarks>
/// <param name="action">The work to run.</param>
public void Run ( Action < Realm > action )
{
if ( ThreadSafety . IsUpdateThread )
action ( Context ) ;
else
{
2022-01-21 15:40:20 +08:00
using ( var realm = createContext ( ) )
2022-01-21 00:33:45 +08:00
action ( realm ) ;
}
}
2022-01-21 16:08:02 +08:00
/// <summary>
/// Write changes to realm.
/// </summary>
/// <remarks>
/// Handles correct context management and transaction committing automatically.
/// </remarks>
/// <param name="action">The work to run.</param>
public void Write ( Action < Realm > action )
{
if ( ThreadSafety . IsUpdateThread )
Context . Write ( action ) ;
else
{
using ( var realm = createContext ( ) )
realm . Write ( action ) ;
}
}
2022-01-21 17:50:25 +08:00
private readonly Dictionary < Func < Realm , IDisposable ? > , IDisposable ? > subscriptionActions = new Dictionary < Func < Realm , IDisposable ? > , IDisposable ? > ( ) ;
2022-01-21 17:13:21 +08:00
/// <summary>
/// Run work on realm that will be run every time the update thread realm context gets recycled.
/// </summary>
2022-01-21 17:50:25 +08:00
/// <param name="action">The work to run. Return value should be an <see cref="IDisposable"/> from QueryAsyncWithNotifications, or an <see cref="InvokeOnDisposal"/> to clean up any bindings.</param>
/// <returns>An <see cref="IDisposable"/> which should be disposed to unsubscribe any inner subscription.</returns>
public IDisposable Register ( Func < Realm , IDisposable ? > action )
2022-01-21 17:13:21 +08:00
{
2022-01-21 17:50:25 +08:00
if ( ! ThreadSafety . IsUpdateThread )
throw new InvalidOperationException ( @ $"{nameof(Register)} must be called from the update thread." ) ;
2022-01-21 17:13:21 +08:00
2022-01-21 17:50:25 +08:00
registerSubscription ( action ) ;
2022-01-23 17:13:28 +08:00
// This token is returned to the consumer only.
// It will cause the registration to be permanently removed.
2022-01-21 17:50:25 +08:00
return new InvokeOnDisposal ( ( ) = >
2022-01-21 17:13:21 +08:00
{
2022-01-23 17:00:24 +08:00
lock ( contextLock )
2022-01-21 17:50:25 +08:00
{
2022-01-23 17:00:24 +08:00
if ( subscriptionActions . TryGetValue ( action , out var unsubscriptionAction ) )
{
unsubscriptionAction ? . Dispose ( ) ;
subscriptionActions . Remove ( action ) ;
}
2022-01-21 17:50:25 +08:00
}
} ) ;
}
private void registerSubscription ( Func < Realm , IDisposable ? > action )
{
Debug . Assert ( ThreadSafety . IsUpdateThread ) ;
2022-01-21 19:45:10 +08:00
// Get context outside of flag update to ensure beyond doubt this can't be cyclic.
var realm = Context ;
2022-01-21 17:50:25 +08:00
lock ( contextLock )
2022-01-21 17:13:21 +08:00
{
2022-01-23 17:13:28 +08:00
Debug . Assert ( ! customSubscriptionActions . TryGetValue ( action , out var found ) | | found = = null ) ;
2022-01-21 17:13:21 +08:00
current_thread_subscriptions_allowed . Value = true ;
2022-01-21 19:45:10 +08:00
subscriptionActions [ action ] = action ( realm ) ;
2022-01-21 17:13:21 +08:00
current_thread_subscriptions_allowed . Value = false ;
}
}
2022-01-23 17:13:28 +08:00
/// <summary>
/// Unregister all subscriptions when the realm context is to be recycled.
/// Subscriptions will still remain and will be re-subscribed when the realm context returns.
/// </summary>
private void unregisterAllSubscriptions ( )
{
foreach ( var action in subscriptionActions )
{
action . Value ? . Dispose ( ) ;
subscriptionActions [ action . Key ] = null ;
}
}
2022-01-21 15:40:20 +08:00
private Realm createContext ( )
2021-01-12 13:36:35 +08:00
{
2021-10-15 12:58:14 +08:00
if ( isDisposed )
2021-10-01 02:46:53 +08:00
throw new ObjectDisposedException ( nameof ( RealmContextFactory ) ) ;
2021-11-29 17:26:37 +08:00
bool tookSemaphoreLock = false ;
2021-06-24 13:37:26 +08:00
try
{
2021-11-29 16:38:42 +08:00
if ( ! currentThreadCanCreateContexts . Value )
2021-11-29 17:26:37 +08:00
{
2021-11-29 15:18:57 +08:00
contextCreationLock . Wait ( ) ;
2021-11-29 17:52:29 +08:00
currentThreadCanCreateContexts . Value = true ;
2021-11-29 17:26:37 +08:00
tookSemaphoreLock = true ;
}
else
{
// the semaphore is used to handle blocking of all context creation during certain periods.
// once the semaphore has been taken by this code section, it is safe to create further contexts on the same thread.
// this can happen if a realm subscription is active and triggers a callback which has user code that calls `CreateContext`.
}
2021-01-22 16:28:47 +08:00
2021-06-24 13:37:26 +08:00
contexts_created . Value + + ;
2021-01-12 13:36:35 +08:00
2021-09-30 22:42:40 +08:00
return Realm . GetInstance ( getConfiguration ( ) ) ;
2021-06-24 13:37:26 +08:00
}
finally
2021-01-12 13:36:35 +08:00
{
2021-11-29 17:26:37 +08:00
if ( tookSemaphoreLock )
{
contextCreationLock . Release ( ) ;
currentThreadCanCreateContexts . Value = false ;
}
2021-06-24 13:37:26 +08:00
}
2021-01-12 13:36:35 +08:00
}
2021-09-30 22:42:40 +08:00
private RealmConfiguration getConfiguration ( )
2021-06-28 15:14:14 +08:00
{
2022-01-18 10:39:22 +08:00
// This is currently the only usage of temporary files at the osu! side.
// If we use the temporary folder in more situations in the future, this should be moved to a higher level (helper method or OsuGameBase).
string tempPathLocation = Path . Combine ( Path . GetTempPath ( ) , @"lazer" ) ;
if ( ! Directory . Exists ( tempPathLocation ) )
Directory . CreateDirectory ( tempPathLocation ) ;
2021-09-30 22:42:40 +08:00
return new RealmConfiguration ( storage . GetFullPath ( Filename , true ) )
{
SchemaVersion = schema_version ,
MigrationCallback = onMigration ,
2022-01-18 10:39:22 +08:00
FallbackPipePath = tempPathLocation ,
2021-09-30 22:42:40 +08:00
} ;
2021-06-28 15:14:14 +08:00
}
2021-01-13 17:24:19 +08:00
private void onMigration ( Migration migration , ulong lastSchemaVersion )
2021-01-07 13:07:36 +08:00
{
2021-11-22 17:37:28 +08:00
for ( ulong i = lastSchemaVersion + 1 ; i < = schema_version ; i + + )
2021-11-04 17:39:23 +08:00
applyMigrationsForVersion ( migration , i ) ;
}
2021-11-22 17:37:28 +08:00
private void applyMigrationsForVersion ( Migration migration , ulong targetVersion )
2021-11-04 17:39:23 +08:00
{
2021-11-22 17:37:28 +08:00
switch ( targetVersion )
2021-11-04 17:32:50 +08:00
{
2021-11-04 17:39:23 +08:00
case 7 :
2021-11-19 18:07:21 +08:00
convertOnlineIDs < BeatmapInfo > ( ) ;
convertOnlineIDs < BeatmapSetInfo > ( ) ;
convertOnlineIDs < RulesetInfo > ( ) ;
2021-11-04 17:32:50 +08:00
2021-11-04 17:39:23 +08:00
void convertOnlineIDs < T > ( ) where T : RealmObject
{
2021-11-05 16:01:00 +08:00
string className = getMappedOrOriginalName ( typeof ( T ) ) ;
2021-11-04 17:32:50 +08:00
2021-11-04 17:39:23 +08:00
// version was not bumped when the beatmap/ruleset models were added
// therefore we must manually check for their presence to avoid throwing on the `DynamicApi` calls.
if ( ! migration . OldRealm . Schema . TryFindObjectSchema ( className , out _ ) )
return ;
2021-11-04 17:32:50 +08:00
2021-11-04 17:39:23 +08:00
var oldItems = migration . OldRealm . DynamicApi . All ( className ) ;
var newItems = migration . NewRealm . DynamicApi . All ( className ) ;
2021-11-04 17:32:50 +08:00
2021-11-04 17:39:23 +08:00
int itemCount = newItems . Count ( ) ;
2021-11-04 17:32:50 +08:00
2021-11-04 17:39:23 +08:00
for ( int i = 0 ; i < itemCount ; i + + )
{
dynamic? oldItem = oldItems . ElementAt ( i ) ;
dynamic? newItem = newItems . ElementAt ( i ) ;
2021-10-29 10:14:22 +08:00
2021-11-04 17:39:23 +08:00
long? nullableOnlineID = oldItem ? . OnlineID ;
newItem . OnlineID = ( int ) ( nullableOnlineID ? ? - 1 ) ;
}
}
2021-10-29 10:14:22 +08:00
2021-11-04 17:39:23 +08:00
break ;
2021-10-29 10:14:22 +08:00
2021-11-04 17:39:23 +08:00
case 8 :
// Ctrl -/+ now adjusts UI scale so let's clear any bindings which overlap these combinations.
// New defaults will be populated by the key store afterwards.
var keyBindings = migration . NewRealm . All < RealmKeyBinding > ( ) ;
2021-10-18 14:35:51 +08:00
2021-11-04 17:39:23 +08:00
var increaseSpeedBinding = keyBindings . FirstOrDefault ( k = > k . ActionInt = = ( int ) GlobalAction . IncreaseScrollSpeed ) ;
if ( increaseSpeedBinding ! = null & & increaseSpeedBinding . KeyCombination . Keys . SequenceEqual ( new [ ] { InputKey . Control , InputKey . Plus } ) )
migration . NewRealm . Remove ( increaseSpeedBinding ) ;
2021-10-18 14:35:51 +08:00
2021-11-04 17:39:23 +08:00
var decreaseSpeedBinding = keyBindings . FirstOrDefault ( k = > k . ActionInt = = ( int ) GlobalAction . DecreaseScrollSpeed ) ;
if ( decreaseSpeedBinding ! = null & & decreaseSpeedBinding . KeyCombination . Keys . SequenceEqual ( new [ ] { InputKey . Control , InputKey . Minus } ) )
migration . NewRealm . Remove ( decreaseSpeedBinding ) ;
break ;
case 9 :
// Pretty pointless to do this as beatmaps aren't really loaded via realm yet, but oh well.
2021-11-19 18:07:21 +08:00
string metadataClassName = getMappedOrOriginalName ( typeof ( BeatmapMetadata ) ) ;
2021-11-09 13:51:06 +08:00
// May be coming from a version before `RealmBeatmapMetadata` existed.
2021-11-09 16:42:03 +08:00
if ( ! migration . OldRealm . Schema . TryFindObjectSchema ( metadataClassName , out _ ) )
2021-11-09 13:51:06 +08:00
return ;
2021-11-09 16:42:03 +08:00
var oldMetadata = migration . OldRealm . DynamicApi . All ( metadataClassName ) ;
2021-11-19 18:07:21 +08:00
var newMetadata = migration . NewRealm . All < BeatmapMetadata > ( ) ;
2021-10-18 14:35:51 +08:00
2021-11-05 17:24:07 +08:00
int metadataCount = newMetadata . Count ( ) ;
2021-10-18 14:35:51 +08:00
2021-11-05 17:24:07 +08:00
for ( int i = 0 ; i < metadataCount ; i + + )
2021-10-18 14:35:51 +08:00
{
2021-11-05 16:02:23 +08:00
dynamic? oldItem = oldMetadata . ElementAt ( i ) ;
var newItem = newMetadata . ElementAt ( i ) ;
2021-10-18 14:35:51 +08:00
2021-11-04 17:39:23 +08:00
string username = oldItem . Author ;
newItem . Author = new RealmUser
{
Username = username
} ;
2021-10-18 14:35:51 +08:00
}
2021-11-04 17:39:23 +08:00
2021-11-22 17:07:28 +08:00
break ;
case 10 :
string rulesetSettingClassName = getMappedOrOriginalName ( typeof ( RealmRulesetSetting ) ) ;
2021-11-28 22:00:40 +08:00
if ( ! migration . OldRealm . Schema . TryFindObjectSchema ( rulesetSettingClassName , out _ ) )
return ;
2021-11-22 17:07:28 +08:00
var oldSettings = migration . OldRealm . DynamicApi . All ( rulesetSettingClassName ) ;
var newSettings = migration . NewRealm . All < RealmRulesetSetting > ( ) . ToList ( ) ;
for ( int i = 0 ; i < newSettings . Count ; i + + )
{
dynamic? oldItem = oldSettings . ElementAt ( i ) ;
var newItem = newSettings . ElementAt ( i ) ;
long rulesetId = oldItem . RulesetID ;
2021-11-23 15:27:28 +08:00
string? rulesetName = getRulesetShortNameFromLegacyID ( rulesetId ) ;
2021-11-22 17:34:04 +08:00
if ( string . IsNullOrEmpty ( rulesetName ) )
migration . NewRealm . Remove ( newItem ) ;
else
newItem . RulesetName = rulesetName ;
}
break ;
case 11 :
string keyBindingClassName = getMappedOrOriginalName ( typeof ( RealmKeyBinding ) ) ;
2021-11-28 22:00:40 +08:00
if ( ! migration . OldRealm . Schema . TryFindObjectSchema ( keyBindingClassName , out _ ) )
return ;
2021-11-22 17:34:04 +08:00
var oldKeyBindings = migration . OldRealm . DynamicApi . All ( keyBindingClassName ) ;
var newKeyBindings = migration . NewRealm . All < RealmKeyBinding > ( ) . ToList ( ) ;
for ( int i = 0 ; i < newKeyBindings . Count ; i + + )
{
dynamic? oldItem = oldKeyBindings . ElementAt ( i ) ;
var newItem = newKeyBindings . ElementAt ( i ) ;
if ( oldItem . RulesetID = = null )
continue ;
long rulesetId = oldItem . RulesetID ;
2021-11-23 16:48:25 +08:00
string? rulesetName = getRulesetShortNameFromLegacyID ( rulesetId ) ;
2021-11-22 17:07:28 +08:00
if ( string . IsNullOrEmpty ( rulesetName ) )
migration . NewRealm . Remove ( newItem ) ;
else
newItem . RulesetName = rulesetName ;
}
2021-11-04 17:39:23 +08:00
break ;
2021-10-18 14:35:51 +08:00
}
2021-01-13 17:24:19 +08:00
}
2021-01-07 13:07:36 +08:00
2021-11-23 17:13:05 +08:00
private string? getRulesetShortNameFromLegacyID ( long rulesetId ) = >
2021-11-23 18:15:52 +08:00
efContextFactory ? . Get ( ) . RulesetInfo . FirstOrDefault ( r = > r . ID = = rulesetId ) ? . ShortName ;
2021-11-23 15:27:28 +08:00
2022-01-19 10:56:44 +08:00
public void CreateBackup ( string backupFilename )
2022-01-18 13:30:41 +08:00
{
using ( BlockAllOperations ( ) )
{
2022-01-19 10:56:44 +08:00
Logger . Log ( $"Creating full realm database backup at {backupFilename}" , LoggingTarget . Database ) ;
2022-01-20 23:46:47 +08:00
int attempts = 10 ;
while ( attempts - - > 0 )
{
try
{
using ( var source = storage . GetStream ( Filename ) )
using ( var destination = storage . GetStream ( backupFilename , FileAccess . Write , FileMode . CreateNew ) )
source . CopyTo ( destination ) ;
return ;
}
catch ( IOException )
{
// file may be locked during use.
Thread . Sleep ( 500 ) ;
}
}
2022-01-18 13:30:41 +08:00
}
}
2021-09-30 22:42:40 +08:00
/// <summary>
/// Flush any active contexts and block any further writes.
/// </summary>
/// <remarks>
/// This should be used in places we need to ensure no ongoing reads/writes are occurring with realm.
/// ie. to move the realm backing file to a new location.
/// </remarks>
/// <returns>An <see cref="IDisposable"/> which should be disposed to end the blocking section.</returns>
public IDisposable BlockAllOperations ( )
2021-01-22 16:28:47 +08:00
{
2021-10-15 12:58:14 +08:00
if ( isDisposed )
2021-09-30 22:42:40 +08:00
throw new ObjectDisposedException ( nameof ( RealmContextFactory ) ) ;
2021-06-29 19:21:31 +08:00
2022-01-21 21:40:18 +08:00
SynchronizationContext syncContext ;
2021-10-01 00:32:28 +08:00
try
{
contextCreationLock . Wait ( ) ;
2021-10-01 02:53:33 +08:00
lock ( contextLock )
{
2022-01-18 13:30:32 +08:00
if ( ! ThreadSafety . IsUpdateThread & & context ! = null )
throw new InvalidOperationException ( @ $"{nameof(BlockAllOperations)} must be called from the update thread." ) ;
2022-01-21 21:40:18 +08:00
syncContext = SynchronizationContext . Current ;
2022-01-23 17:13:28 +08:00
unregisterAllSubscriptions ( ) ;
2022-01-21 21:40:18 +08:00
2022-01-18 13:30:32 +08:00
Logger . Log ( @"Blocking realm operations." , LoggingTarget . Database ) ;
2021-10-01 02:53:33 +08:00
context ? . Dispose ( ) ;
context = null ;
}
2021-10-01 00:32:28 +08:00
const int sleep_length = 200 ;
int timeout = 5000 ;
2021-01-22 16:28:47 +08:00
2022-01-18 15:05:12 +08:00
try
2021-10-01 00:32:28 +08:00
{
2022-01-18 15:05:12 +08:00
// see https://github.com/realm/realm-dotnet/discussions/2657
while ( ! Compact ( ) )
{
Thread . Sleep ( sleep_length ) ;
timeout - = sleep_length ;
2021-06-29 19:21:31 +08:00
2022-01-18 15:05:12 +08:00
if ( timeout < 0 )
throw new TimeoutException ( @"Took too long to acquire lock" ) ;
}
}
2022-01-19 09:58:59 +08:00
catch ( RealmException e )
2022-01-18 15:05:12 +08:00
{
// Compact may fail if the realm is in a bad state.
// We still want to continue with the blocking operation, though.
Logger . Log ( $"Realm compact failed with error {e}" , LoggingTarget . Database ) ;
2021-10-01 00:32:28 +08:00
}
}
catch
{
contextCreationLock . Release ( ) ;
throw ;
}
return new InvokeOnDisposal < RealmContextFactory > ( this , factory = >
2021-09-30 22:42:40 +08:00
{
factory . contextCreationLock . Release ( ) ;
Logger . Log ( @"Restoring realm operations." , LoggingTarget . Database ) ;
2022-01-21 21:40:18 +08:00
// Post back to the update thread to revive any subscriptions.
syncContext ? . Post ( _ = > ensureUpdateContext ( ) , null ) ;
2021-10-01 00:32:28 +08:00
} ) ;
2021-01-15 13:26:06 +08:00
}
2021-11-05 16:01:00 +08:00
// https://github.com/realm/realm-dotnet/blob/32f4ebcc88b3e80a3b254412665340cd9f3bd6b5/Realm/Realm/Extensions/ReflectionExtensions.cs#L46
private static string getMappedOrOriginalName ( MemberInfo member ) = > member . GetCustomAttribute < MapToAttribute > ( ) ? . Mapping ? ? member . Name ;
2021-10-15 12:58:14 +08:00
private bool isDisposed ;
public void Dispose ( )
2021-06-28 15:14:14 +08:00
{
2021-10-01 02:46:53 +08:00
lock ( contextLock )
{
context ? . Dispose ( ) ;
}
2021-09-30 22:42:40 +08:00
2021-10-15 12:58:14 +08:00
if ( ! isDisposed )
2021-06-28 15:14:14 +08:00
{
2021-10-01 00:32:28 +08:00
// intentionally block context creation indefinitely. this ensures that nothing can start consuming a new context after disposal.
contextCreationLock . Wait ( ) ;
2021-09-30 22:42:40 +08:00
contextCreationLock . Dispose ( ) ;
2021-06-28 15:14:14 +08:00
2021-10-15 12:58:14 +08:00
isDisposed = true ;
}
2021-06-28 15:14:14 +08:00
}
2021-01-07 13:07:36 +08:00
}
}