How to correctly manipulate subscriptions in AppServices flexible sync

We have a .NET MAUI app that uses AppServices flexible sync.

We manage the realm instance through DI container and the realm instance has a “Scoped” lifecycle. Usually, each screen in the app will have its own scope. Though, there are scenarios where we have heavy operations and we explicitly create the scope in a background thread to get thread-specific realm instance. Thus, at any given point in time, there may be several active realm instances inside the app.

We are currently working on optimizing the data that gets synced to the users device. One of the global query filters that we have is the language code - user gets only the content corresponding to the language he has selected in the app. Thus, when the language changes, we need to update the subscriptions.

First thing that I did, is listening to the language change events in the class that manages the realm instance and update the subscriptions accordingly. It looks like something like this

    public void SetPreferredLanguageCodes(Realm realm, params string[] iso6391LanguageCode)
    {
        string languageCodesFilter = $"Iso6391LanguageCode IN {{{string.Join(",", iso6391LanguageCode)}}}";
        realm.Subscriptions.Update(() =>
        {
            ...
            realm.Subscriptions.Add(realm.All<Author>()
                                         .Filter(languageCodesFilter),
                                    new SubscriptionOptions { Name = "AllAuthors" });
            realm.Subscriptions.Add(realm.All<Video>()
                                         .Filter(languageCodesFilter),
                                    new SubscriptionOptions { Name = "AllVideos" });
           ....
        });
    }

The issue is, I have several instances and this method is invoked for each instance of the realm and results in the following exception

You can't Update/UpdateAsync on a subscription set that is already being updated.

I conceptually understand what is says. However, I do not understand the repercussions of this exception. First, the Subscriptions.Update method is a method of a realm instance. Which logically means that each realm may have its own subscriptions. However, something makes me think that all of them share the same “session” under the hood, which ultimately is responsible for managing the subscriptions. If so, why wouldn’t the Subscription.Update be a method of session and not the realm?

What would be the correct way of updating the subscriptions?

Options 1
I would have a singleton class that has a realm instance in it, and I only update the subscriptions in that singleton class. Will this also impact all of the realm instances that are already created? Will the data be updated accordingly inside all of those realm instance?

Option 2
Make the realm instance singleton. This is very undesirable though, as there are occasions where the data should be processed in the background thread and we still need to create a separate realm instance for that.

I do want to understand how conceptually subscriptions work under the hood, and perhaps, I would come up with a better approach.

We should probably clarify this in the exception, but the root cause of this is calling Update inside an Update callback. It’s not obvious to me how this could happen as it’s likely due to stuff omitted from the code snippet, but an example would be:

void DoSomething()
{
    realm.Subscriptions.Update(...);
}

void DoSomethingElse()
{
    realm.Subscriptions.Update(() =>
    {
        DoSomething();
    });
}

If you can share the stacktrace of the exception, that’ll give us some pointers to the callstack that triggers the second update.

For more context, you wouldn’t get this error if you’re calling Update on two different Realm instances, even if they point to the same database - e.g.:

var realm1 = Realm.GetInstance(config);
var realm2 = Realm.GetInstance(config);

realm1.Subscriptions.Update(() =>
{
    realm2.Subscriptions.Update(() => {});
});

Though you’ll probably get a deadlock there since the second update will need to wait for the first one to complete.

If you do the subscription updates on different thread, one thread will be blocked while the other commits the update:

Task.Run(() =>
{
    using var realm = Realm.GetInstance(config);
    realm.Subscriptions.Update(() => { ... });
});

Task.Run(() =>
{
    using var realm = Realm.GetInstance(config);
    realm.Subscriptions.Update(() => { ... });
});

The above code will complete, but it’s non-deterministic which one will complete first - whoever manages to acquire the write lock will execute the entire update block, while the other waits.

Under the hood, SubscriptionSet.Update is equivalent to Realm.Write as it needs to persist the subscription changes in a special area of the database, after which the Sync Client will pick them up and communicate to the server. This is necessary in case the app is offline or crashes while the client is talking to the server and the subscriptions need to be renegotiated when connectivity is restored.

Hey @nirinchev

Thanks a lot for leading me to the right direction. I did a code refactoring and moved the realm.Subscriptions.Add methods inside the realm.Subscriptions.Update method. However, I was already doing that in the method that was calling the SetPreferredLanguageCodes.

Thanks a lot :slight_smile:

This topic was automatically closed 5 days after the last reply. New replies are no longer allowed.