Search code examples
iosxamarinasync-awaitdeadlockazure-mobile-services

IMobileServiceClient.PullAsync deadlock when trying to sync with Azure Mobile Services


I have the classes below.

public class AzureMobileDataContext : IAsyncInitialization
    {
        private static readonly Lazy<AzureMobileDataContext> lazy =
            new Lazy<AzureMobileDataContext> (() => 
                new AzureMobileDataContext(
                    new MobileServiceClient(
                                "http://myservice.azure-mobile.net/",
                                "123456789ABCDEFGHIJKLMNOP")));

        public static AzureMobileDataContext Instance { get { return lazy.Value; } }
        public Task Initialization { get; private set; }
        public IMobileServiceClient Context { get; private set; }

        private Object lockObj = new Object ();
        private static MobileServiceSQLiteStore store;

        public AzureMobileDataContext (IMobileServiceClient context)
        {
            Context = context;
            Initialization = Init ();
            Initialization.ContinueWith (async (antecedent) => {
                await Context.SyncContext.InitializeAsync (store, new MobileServiceSyncHandler ());
            });
        }

        private Task Init ()
        {
            return Task.Run (() => {
                lock (lockObj) {
                    if (!Context.SyncContext.IsInitialized) {
                        try {
                            store = new MobileServiceSQLiteStore ("mysqlite.db3");

                            store.DefineTable<Post> ();
                            store.DefineTable<PostPhotoUrl> ();
                            store.DefineTable<User> ();
                            store.DefineTable<Club> ();
                            store.DefineTable<District> ();
                        } catch (Exception ex) {
                            Debug.WriteLine ("Init: {0}", ex.Message);
                        }
                    }
                }
            });
        }

        public async Task<IMobileServiceSyncTable<TEntity>> GetTableAsync<TEntity> ()
        {
            await Initialization;
            return Context.GetSyncTable<TEntity> ();
        }

        public async Task PushAsync ()
        {
            try {
                await Initialization;
                await Context.SyncContext.PushAsync ();
            } catch (MobileServiceInvalidOperationException invalidOperationEx) {
                Debug.WriteLine (invalidOperationEx.Message);
            } catch (MobileServicePushFailedException pushFailedException) {
                Debug.WriteLine (pushFailedException.Message);
            }
        }

        public async Task PullAsync<TEntity> (IMobileServiceTableQuery<TEntity> query)
        {
            try {
                await Initialization;
                IMobileServiceSyncTable<TEntity> entityTable = await GetTableAsync<TEntity> ();
                await entityTable.PullAsync (typeof(TEntity).ToString (), query); // Never returns, no exception is caught or thrown.
                await entityTable.PurgeAsync ();
            } catch (MobileServiceInvalidOperationException preconditionFailedEx) {
                Debug.WriteLine (preconditionFailedEx.Message);
            } catch (Exception ex) {
                Debug.WriteLine (ex.Message);
            }
        }

        public async Task SyncAsync<TEntity> ()
        {
            await PushAsync ();
            IMobileServiceSyncTable<TEntity> syncTable = await GetTableAsync<TEntity> ();
            await PullAsync (syncTable.CreateQuery ());
        }
    }

I use this singleton from a BaseRepository I have that is a base class for 5 different entity repositories.

public abstract class BaseRepository<TEntity> : IRepository<TEntity> where TEntity : class
    {
        protected AzureMobileDataContext MobileServiceContext { get { return AzureMobileDataContext.Instance; } }

        protected virtual Task PushAsync ()
        {
            return MobileServiceContext.PushAsync ();
        }

        protected virtual Task PullAsync (IMobileServiceTableQuery<TEntity> query)
        {
            return MobileServiceContext.PullAsync (query);
        }

        public virtual async Task<DataObjectResponse<IEnumerable<TEntity>>> FindAsync (Expression<Func<TEntity, bool>> predicate)
        {
            IMobileServiceSyncTable<TEntity> syncTable = await MobileServiceContext.GetTableAsync<TEntity> ();
            await PullAsync (syncTable.CreateQuery ());
            IEnumerable<TEntity> entities = await syncTable.Where (predicate).ToEnumerableAsync ();
            return new DataObjectResponse<IEnumerable<TEntity>> (entities);
        }
}

The users repository.

public class UsersAzureRepository : BaseRepository<User>, IUsersRepository
    {
        public UsersAzureRepository ()
        {
        }

        public async Task<DataObjectResponse<User>> FindByIdAsync (string entityId)
        {
            DataObjectResponse<IEnumerable<User>> users = await FindAsync (p => p.Id == entityId);
            return new DataObjectResponse<User>(users.Data.FirstOrDefault ());
        }
    }

A DataService Facade class containing the GetUserById method.

public async Task<UserModel> GetUserById (string userId)
        {
            DataObjectResponse<User> users = await UsersRepository.FindByIdAsync (userId);
            UserModel userModel = Mapper.Map<User, UserModel> (users.Data);
            return userModel;
        }

Users view model method.

public async Task<UserModel> GetUsersAsync() // testing purposes
        {
            UserModel user = await _dataService.GetUserById("032beb3b-1cbf-4a0d-809c-a25c71139c55"); 
            if (user != null) {
                Debug.WriteLine ("User loaded: {0}", user.Id);
            }
            return user;
        }

Call from an iOS UIViewController.

public async override void ViewDidLoad ()
        {
            base.ViewDidLoad ();

            UserModel user = await ViewModel.GetUsersAsync ();
        }

The AzureMobileDataContext serves more as a thread safe wrapper to the IMobileServiceClient context operations, making sure not multiple threads will try to initialize the database (I had an exception when using it directly to the BaseRepository<T> before).

I'm not so sure from here where might the problem is. I suspect that the wrapper is not the best solution and any recommendations are welcome.

Any other ways to debug the PullAsync method?

[EDIT]

The local SQLite database syncs the table data from the remote service but still the call doesn't return.


Solution

  • The problem was in the Azure Mobile Service, server side.

    I was returning from the TableController an IEnumerable but the SDK uses OData query expressions to do it's own job, returning an IEnumerable is not sufficient, changing to IQueryable fixed this issue of a continuous looping when pulling data.

    I strongly believe that the server return type shouldn't be related to the SDK but this is the way it works.