Handling Sync Errors in .NET

cloud

#1

The Realm help document Errors tells us how to trap sync errors but I cannot see where to apply error handling. As I understand it sync isn’t necessarily happening while our app reads or writes to the local realm. Sync happens in it’s own time on a background thread so where do I trap sync errors?

Can anyone point me to examples of handling Realm Cloud sync errors in .NET?


#2

The .NET example in the docs shows how to setup error handing - precisely because sync happens in the background, you can’t rely on try-catch mechanics to handle errors and instead those are reported through the Session.Error event. It shows a minimal error handling example that can be applied to pretty much all error types. Is there something specific that is unclear about that?


#3

The docs show how to apply error handling but not where. Where do we apply error handling for sync errors?


#4

You can handle errors wherever in your app feels natural - that will depend on the type of the app and the type of action you would like to perform when an error arises. For example, if you have a Xamarin.Forms app that excessively uses a single database for most of its lifetime, you could do that in your App.xaml.cs and redirect the user to the main page. This is a very simplistic code sample that doesn’t adhere to separation of concerns best practices and so on, but should be good enough for illustrative purposes:

public partial class App : Application
{
    // Assuming your app uses a reference to this for Realm operations on the main thread
    public Realm MainThreadRealm { get; private set; }

    public App()
    {
        InitializeComponent();
        MainPage = new NavigationPage(new MainPage());

        SetupErrorHandling();
    }

    void SetupErrorHandling()
    {
        Session.Error += (session, errorArgs)
        {
            var sessionException = (SessionException)errorArgs.Exception;
            if (sessionException.ErrorCode.IsClientResetError())
            {
                var clientResetException = (ClientResetException)errorArgs.Exception;
                // Redirect the user to an error page explaining that the database needs to be
                // redownloaded.
                var errorPage = new ErrorPage();
                
                // Assuming you have a callback on the error page that reopens the Realm
                errorPage.OnDbDownloaded = (realm) =>
                {
                    MainThreadRealm = realm;
                    MainPage = new NavigationPage(new MainPage());
                };
                MainPage = errorPage;
                MainThreadRealm.Dispose();
                clientResetException.InitiateClientReset();

                // Tell the error page to try to reopen the Realm
                errorPage.TryRedownloadRealm();
            }

            // Handle other errors
        }
    }
}

For console apps or services, it may be good enough to just restart the app/service, assuming these errors should be fairly infrequent.


#5

Thank you @nirinchev. In our case the UI knows nothing of Realm. Realm happens in another project n the solution. Each read and write creates and disposes a realm instance. I assume that means we setup error handling each time we call GetRealm. I tried like this

internal static Realm GetRealm()
{
    Uri dataURI = new Uri($"realms://{SERVER}/~/user");
    FullSyncConfiguration config = new FullSyncConfiguration(dataURI, realmUser);
    config.EnableSSLValidation = false;
    Realm realm = Realm.GetInstance(config);
    Session.Error += (session, errorArgs)
        {
        var sessionException = (SessionException)errorArgs.Exception;
        if (sessionException.ErrorCode.IsClientResetError())
        {
            var clientResetException = (ClientResetException)errorArgs.Exception;
            clientResetException.InitiateClientReset();
        }
    }
    return realm;
}

The line

Session.Error += (session, errorArgs)

gives the error

Cannot implicitly convert type ‘(? session, ? errorArgs)’ to ‘System.EventHandler<Realms.ErrorEventArgs>’


#6

Sorry about the error - seems like when I typed the code I forgot the arrow in the event handler. It needs to be:

Session.Error += (session, errorArgs) =>
{
    // ...
};

Regarding setting it up in your app - your approach seems a little dangerous, because a consumer of the GetRealm call will not get notified that the Realm they’re using is suddenly invalid. This is why typically we advise redirecting the user to a temporary screen that doesn’t interact with the Realm file. But I also don’t know how your app is structured - perhaps it’s fine to wrap the entire section that uses Realm in a try-catch and retry the operation if you get an exception that the Realm is disposed.

Another point there is that you’re subscribing to Session.Error every time you open a Realm, but you’re never unsubscribing from it. Apart from being a potential memory leak, this means that when an exception occurs, you’ll have a potentially large number of handlers reacting to it, doing the same thing.


#7

Thanks again @nirinchev. The arrow is also missing in the docs.

The UI doesn’t call GetRealm. The project that references Realm returns helper classes to the UI, not Realm classes. For example the UI might request some data from the OrderService.

public static List<OrderDetail> GetOrderDetails(Order order)
{
    List<OrderDetail> items = new List<OrderDetail>();
    using (Realm realm = RealmService.GetRealm())
    {
....
    }
    return items;
}

Order and OrderDetails are not Realm classes, but helper classes. As you see GetRealm is called within a using statement.

The architecture is a relaxed MVVM in which the UI project has no knowledge of the underlying data store. Data is managed from the services project.

GetOrderDetails and GetRealm are members of the services project. How would you recommend implementing Realm sync error handling in this type of architecture?


#8

Sorry for the delay in responding - the examples I gave you are kind of simplified, but they are applicable to MVVM or whatever architecture your app employs. I imagine you have an app entrypoint that sets up navigation and all that. In there, you subscribe to an event in your persistence service for errors, then tell your navigation service to navigate to an error screen, tell the persistence service to handle the error, and finally navigate back to the main screen (or if safely, to the screen where the user was before). In pseudocode, this would look like:

class App
{
    // Resolved via DI or whatever
    private IPersistenceService persistenceService; 
    private INavigationService navigationService; 

    public void SetupEverything()
    {
        navigationService.Navigate(Screens.Main);
        persistenceService.OnError += (s, e) =>
        {
            navigationService.Navigate(Screens.Error, e);
        });
    }
}

class ErrorViewModel
{
    // Resolved via DI or whatever
    private IPersistenceService persistenceService; 
    private INavigationService navigationService; 

    // It's an opaque object, the VM doesn't need to know what's in there
    private object errorDetails;

    // Bind some label in the UI to this; implement INotifyPropertyChanged
    public ErrorMessage { get; set; }

    public ErrorViewModel(object errorDetails)
    {
        HandleError();
    }

    private async Task HandleError()
    {
         ErrorMessage = persistenceService.GetMessage(errorDetails);
         await persistenceService.HandleError(errorDetails);
         ErrorMessage = "Issue resolved. You'll be navigated to the main screen now.";
         await Task.Delay(3000);

         // If it's safe, you can also try to navigate to the original screen the 
         // user was on before the error.
         navigationService.Navigate(Screens.Main);
    }
}

Again - this is for illustrative purposes only and I can’t really give you anything more specific unless I analyze your app code. And obviously, you should not just proxy all sync errors through the persistence service as a lot of them may be benign or self-recoverable. This example covers mostly client reset or permission denied errors which should be very rare.