Skip to content

Realm usage rules

Dean Herbert edited this page Jan 26, 2022 · 5 revisions

Realm is architected in a way to keep threading and consistency management simple for the developer. As we have our own non-standard methods of threading and a generally convoluted flow of data (where a single instance may be passed over and accessed by multiple threads), access to realm is done so through an abstraction layer that adds additional sanity checks and safeties in place.

Consuming data

If you need temporary access to realm data, for instance to pull out a specific (primitive) value, use RealmAccess.Run:

// fetch via DI
RealmAccess realm;

// use the data inside the Run call
realm.Run(r => 
{
    var keyBinding = r.Find<RealmKeyBinding>(lookup).First();
    this.Tooltip = obj.Text;
});

// or return it for use outside.
// note that this will throw if you try to return a managed object outside of the Run call.
var text = realm.Run(r => r.Find<RealmKeyBinding>(lookup).First().Text);

Usage of realm.Run is intended to be very localised. If you need to store or pass realm managed objects, use a Register method and/or Live<T> (see below).

Modifying data

// Modifying a Live<> instance
Live<RealmKeyBinding> keyBinding;

keyBinding.Write(k => 
{
    k.Text = "new text";
});

// Modifying directly on a realm
RealmAccess realm;

realm.Write(r =>
{
    var keyBinding = r.Find<RealmKeyBinding>(lookup).First();
    keyBinding.Text = "new text";
});

Watching for changes

Realm offers the ability to watch any query for changes. This works as long as the main realm instance is kept alive, which is not always the case in osu!. To make this work for us, we add an extra layer to automatically handle this.

RealmAccess realm;

// subscribe to changes (aka realm's `SubscribeForNotifications`)
var subscription = realm.RegisterForNotifications(r => r.All<RealmKeyBinding>(), keyBindingsChanged);

// make sure to dispose when done.
subscription.Dispose();

private void keyBindingsChanged(IRealmCollection<RealmKeyBinding> sender, ChangeSet changes, Exception error)
{
    // all callbacks are fired on the update thread, but not local to the current drawable.
    // if this is important, using an additional `Schedule` is advised.

    if (changes == null)
    {
        // changes will be null once initially after subscription (realm behaviour).
        // it will also be null when the update thread realm is recycled (our behaviour).
        //  - during a realm recycle operation, an empty result set will be returned via `sender`.
        //  - after a recycle operation, the subscription will be re-established, causing the initial realm response to arrive a second time.

        // put simply, when a null `ChangeSet` arrives a component should clear whatever it's displaying 
        // and update with the new contents of `sender`.
    }
}

Custom registration can also handle more advanced cases such as binding to a specific property:

RealmAccess realm;

RealmKeyBinding keyBinding;

var subscription = realm.RegisterCustomSubscription(r =>
{
    keyBinding = r.All<RealmKeyBinding>().First();
    keyBinding.PropertyChanged += onPropertyChanged;

    return new InvokeOnDisposal(() =>
    {
        // perform whatever unsubscription is required.
        keyBinding.PropertyChanged -= onPropertyChanged;
        // ensure on subscription loss that the realm data is invalidated from any fields/properties.
        keyBinding = null;
    });
});

private void onPropertyChanged(sender, args) =>
{
    // all callbacks are fired on the update thread, but not local to the current drawable.
    // if this is important, using an additional `Schedule` is advised.
    if (args.PropertyName == nameof(binding.KeyCombinationString))
        Schedule(() => this.Tooltip = binding.Thing);
};

// make sure to dispose when done.
subscription.Dispose();

Note that when subscribing to changes, it is recommended to not locally use Live<T> as the subscription handling ensures that all (update thread) access will be serviced by valid realm instances. That said, if passing the results of a subscription to another class, Live<T> should still be employed.

Storing and passing data

Realm generally operates bound to a single thread. Any asynchronous usage requires a thread-local realm context. To keep things in line with the simplicity paradigm that realm stands for, minimal thread management is encapsulated within the Live<T> class.

Using Live<T> is imperative when storing data to a field or property (that isn't managed by a subscription as above) or when passing data between classes.

RealmAccess realm;

var liveKeyBinding = realm.Run(r => r.Find<RealmKeyBinding>(lookup).First().ToLive(r));

Task.Run(() => 
{
    // can now be used on any thread
    string text = liveKeyBinding.PerformRead(k => k.Text);
});