Akavache icon indicating copy to clipboard operation
Akavache copied to clipboard

Crash at System.Reactive.Linq.Observable.SelectMany

Open pieterdhondt opened this issue 9 years ago • 23 comments

I randomly get this crash sometimes when using GetAndFetchLatest (in Xamarin.Android and Xamarin.iOS) and I can't really see why it is happening. Any thoughts on what the reason for this can be?

Xamarin caused by: android.runtime.JavaProxyThrowable: System.ArgumentNullException: Value cannot be null. Parameter name: source at System.Reactive.Linq.Observable.SelectMany[TSource,TResult] (IObservable 1 source, System.Func 2 selector) <0xd46f6ca8 + 0x000a8> in <filename unknown>:0 at Akavache.JsonSerializationMixin.GetObject[T] (IBlobCache This, System.String key) <0xd46f60f8 + 0x0017f> in <filename unknown>:0

at Akavache.JsonSerializationMixin.GetAndFetchLatest[T] (IBlobCache This, System.String key, System.Func 1 fetchFunc, System.Func 2 fetchPredicate, Nullable 1 absoluteExpiration, Boolean shouldInvalidateOnError) <0xd46f4e58 + 0x0039b> in <filename unknown>:0

pieterdhondt avatar Jun 06 '16 07:06 pieterdhondt

Can you post the code where this crash occurs?

flagbug avatar Jun 06 '16 07:06 flagbug

Sure.

public async void GetTransactions (bool forceRefresh = false)
        {
            NoDataFound = false;
            SetProgressState (true);

            if (forceRefresh && App.IsNetworkRechable) {
                await BlobCache.LocalMachine.InvalidateObject<List<QuadriDCMTransaction>> ($"timeline-{Project.Id}");
            }

            var startDate = EndDate.AddDays (-DaysBack);

            var cache = BlobCache.LocalMachine;

            if (App.IsNetworkRechable) {
                cache
                    .GetAndFetchLatest (
                        $"timeline-{Project.Id}",
                        () => EasyAccessClient.Instance.EasyAccessService.GetTimeline (Project.Id, startDate, EndDate),
                        offset => {
                            TimeSpan elapsed = DateTimeOffset.Now - offset;
                            return elapsed > new TimeSpan (hours: 0, minutes: 0, seconds: 10);
                        })
                    .Catch (Observable.Return (new List<QuadriDCMTransaction> ()))
                    .Subscribe (
                     t => {
                            Debug.WriteLine ("Subscribed Timeline ready!");
                            UpdateTransactions (t);
                            SetProgressState (false);
                     })
                    .DisposeWith (SubscriptionDisposables);

            } else {
                ...
            }
        }

pieterdhondt avatar Jun 06 '16 07:06 pieterdhondt

Can't see anything wrong with that. If you could somehow create a sample that reproduces this issue, that would be great!

flagbug avatar Jun 06 '16 08:06 flagbug

I'm seeing this error as well with similar code(GetAndFetchLatest). It's not 100% repro however, it's something we're seeing in our crash logs.

hartra344 avatar Jun 07 '16 20:06 hartra344

Yes same here, I tried to make a reproducible example but it's not easy. It's also a crash which occurs quite often in the HockeyApp crash logs.

pieterdhondt avatar Jun 07 '16 21:06 pieterdhondt

I believe I am seeing the same issue. For me, the exception (System.ArgumentNullException: Value cannot be null.Parameter name: source) is a repeatable fault which occurs only after the App is resumed after being suspended. (Note: I am relatively new to X-Forms so it is entirely possible that, in my case, it is operator error)

I have attached a sample VS 2015 Xamarin Forms project (Droid only) which is failing reliably on my system. Here are the steps to reproduce.

  • Target Nexus 4 (KitKat) (Android 4.4 - API 19) using the Xamarin Android Player)
  • Start the app in Debug mode. It should startup and display "We have 5 cats!"
  • Suspend the app using the Back button (Important... don't use the Home button)
  • Restart the app by selecting the app icon on the emulator. The app should throw the exception on the call to GetOrFetchObject.

App3.zip

jimnorth55 avatar Jun 27 '16 18:06 jimnorth55

I worked around this by moving the call to BlobCache.Shutdown().Wait(); within the OnDestroy of my main activity, rather than OnStop.

kpespisa avatar Jul 26 '16 20:07 kpespisa

2 hours debugging the exact same error message here. Moving BlobCache.Shutdown().Wait(); to OnDestroy fixed my issue too. A less cryptic error message would be nice to see.

Thanks kpespisa for posting your solution.

mike-rowley avatar Jul 29 '16 21:07 mike-rowley

One gotcha. I noticed when testing my app, when I used the device back button to exit the app, and then I return to the app, I get the same error.

Apparently onDestroy gets called in that case so putting the call to BlobCache.Shutdown isn't completely safe their either.

I'm not a regular Android user, though, so I don't know how common it is to use that device back button.

A better solution would be to keep the Shutdown call in the onStop handler, and reinitialize the cache when resuming. However I don't know how to reinitialize.

For now, I've completely removed the call to Shutdown.

On iOS there is a WillTerminate method you can override that works perfectly but on Android there doesn't seem to be an equivalent.

kpespisa avatar Jul 29 '16 22:07 kpespisa

@kpespisa Thanks for the comments. My app also has similar issues and I'm still not clear where should put the Shutdown.Wait.

For iOS I found below links: https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplicationDelegate_Protocol/index.html

applicationWillTerminate: (Called only when the app is running. This method is not called if the app is suspended.)

https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/iPhoneOSKeys.html#//apple_ref/doc/uid/TP40009252-SW23

When the value of this key (UIApplicationExitsOnSuspend) is YES, the app is terminated and purged from memory instead of moved to the background. If this key is not present, or is set to NO, the app moves to the background as usual.

It seems in come case, the willTerminate won't be called and app should also put the Shutdown in DidEnterBackground?

Anyone can clarify what's the best place to put the initialization and shutdown in Xamarin.Android/Xamarin.iOS and Xamarin.Forms? @flagbug

jzhouw avatar Aug 14 '16 16:08 jzhouw

I am too experiencing the same issue. I have an issue where I can reproduce this reliably (but cannot share the code, sorry). But when looking in the debugger, in the cases where this exception is about to occur, BlobCache.LocalMachine is an instance of ShutdownBlobCache.

The problem goes away by not calling BlobCache.Shutdown().Wait() at OnDestroy of MainActivity.

@paulcbetts What is the suggested method to call Shutdown?

poimis avatar Dec 21 '16 11:12 poimis

I'm having a similar issue with Xamarin Forms, were in shared code I don't have a shutdown method. Only on sleep, without doing platform specific code.

Would it be possible to add a feature to allow you to re-initialize the BlobCache after it's been shutdown?

bmacombe avatar Jan 04 '17 03:01 bmacombe

Hi, same here.

Is there an alternative to static BlobCache.XXXX ? For example, we could instantiate directly an IBlobCache and keep control over that instance.

Xamarin.Forms provides us 3 events to be aware about app lifecycle : OnStart, OnResume and OnSleep, so when app is going to sleep, we can just call Flush() on the BlobCache instance for example. Or, maybe we can Dispose() the instance (or Shutdown it ?), then re-instanciating it when app resumes.

@paulcbetts Any advice about that and getting rid of BlobCache static class ?

XavierQuincieux avatar Jan 04 '17 22:01 XavierQuincieux

@XavierQuincieux look at the static implementation. It's super simple. Hint: IBlobCache.

Look at the ctor at https://github.com/flagbug/Espera/blob/master/Espera.Core/Settings/CoreSettings.cs

@bmacombe i'm not keen on adding smarts/lifecycle awareness to Akavache and prefer to empower the consumer of Akavache to control the scenarios.

ghuntley avatar Jan 04 '17 23:01 ghuntley

See also https://github.com/flagbug/Lager/blob/master/Lager/SettingsStorage.cs

ghuntley avatar Jan 04 '17 23:01 ghuntley

Thanks for your quick answer.

Here is what I ended up doing in my Xamarin Forms app.

In the App class, I call the following code in OnStart and OnResume methods:

var fs = Locator.Current.GetService<IFilesystemProvider>(); fs.CreateRecursive(fs.GetDefaultLocalMachineCacheDirectory()) .SubscribeOn(BlobCache.TaskpoolScheduler) .Wait();

BlobCache.LocalMachine = new SQLitePersistentBlobCache(Path.Combine(fs.GetDefaultLocalMachineCacheDirectory(), "blobs.db"));

The first part is what Akavache use to get the directory holding the db file, so I just copy that. Then, I create a new instance of BlobCache.LocalMachine. So each time we open the app or we're resuming, a new instance of LocalMachine is created.

Of course, we need to release it properly when the app goes background or terminate. We do this in the OnSleep method, which is called whatever event make the app not in foreground anymore.

BlobCache.LocalMachine.Dispose(); BlobCache.LocalMachine.Shutdown.Wait();

XavierQuincieux avatar Jan 05 '17 08:01 XavierQuincieux

@XavierQuincieux I have noticed that your code doesn't work, in current akavache there is a static private bool on BlobCache called shutdownRequested, if this is true then you will always get a ShutdownBlobCache from any of the BlobCache properties.

The easiest way to reproduce this bug is to set the settings -> developer options ->Don't keep activities to true on the android system you're testing on.

paul-kiar avatar Jan 30 '17 22:01 paul-kiar

This is my take on the code to resolve this issue:

        Akavache.BlobCache.Shutdown().Wait();  // this calls the shutdown code
        Akavache.BlobCache.LocalMachine = null; // clear out the static cached variables
        Akavache.BlobCache.Secure = null;
        Akavache.BlobCache.UserAccount = null;
        FieldInfo fi = typeof(Akavache.BlobCache).GetField("shutdownRequested", BindingFlags.Static | BindingFlags.NonPublic );
        fi.SetValue(null, false); // reset the private flag
        Splat.Locator.Current = new Splat.ModernDependencyResolver(); // create a new dependency resolver so akavache has to create new objects when the app is resurrected.

This code is in the MainActivity.OnDestroy override

paul-kiar avatar Jan 30 '17 22:01 paul-kiar

@paul-kiar The shutdownRequested boolean exists in the BlobCache class, but not in the implementations of IBlobCache, that's why I call Shutdown() on a cache instance, not on BlobCache.

XavierQuincieux avatar Jan 30 '17 23:01 XavierQuincieux

It is very confusing. what is the conclusion here? @XavierQuincieux is your solution work? can we use it safely? Does it work for sleep-resume case as well?

EmilAlipiev avatar Feb 20 '17 23:02 EmilAlipiev

@EmilAlipiev @XavierQuincieux 's solution works.

paul-kiar avatar Feb 21 '17 14:02 paul-kiar

@EmilAlipiev in the the Readme of Akavache you can read this :

What's this Global Variable nonsense? Why can't I use $FAVORITE_IOC_LIBRARY

You totally can. Just instantiate SQLitePersistentBlobCache or SQLiteEncryptedBlobCache instead - the static variables are there just to make it easier to get started.

To me, it means that the BlobCache class is here just to help us to start with Akavache, but it's not necessarily meant to be the only way. It does not fit right with xamarin forms app lifecycle.

You can create your own BlobCache class that is XF compliant, it should not be a big deal.

@paul-kiar It works but sometimes, I get an exception that the local cache object I used is disposed. I guess it happens when a user re-enter the app and some code is executed for some reason before the call to OnRestart. A solution could be to still dispose IBlocCache objects in OnSleep, but instead of recreating it in OnRestart and OnStart, lazy-create it when needed, after cheching if the IBlocCache object is null or disposed.

XavierQuincieux avatar Feb 21 '17 14:02 XavierQuincieux

have the same issue. press back from MainActivity. OnDestroy gets fired, and BlobCache.Shutdown called. App is still in the Android App list. Activating it triggers the MainActivity.OnCreate(), where BlobCache.EnsureInitialized() is called, but the subsequent nullreferenceexception occurs when trying to use BlobCache.GetAndFetchLatest()

InquisitorJax avatar Mar 18 '17 09:03 InquisitorJax