ThirtyInch icon indicating copy to clipboard operation
ThirtyInch copied to clipboard

Fix crash when calling sendToView while the Presenter attaches the view

Open passsy opened this issue 7 years ago • 7 comments

@k0shk0sh discovered a one in a million crash in production

Fatal Exception: java.lang.IllegalStateException: no ui thread executor available
       at net.grandcentrix.thirtyinch.TiPresenter.runOnUiThread(TiPresenter.java:371)
       at net.grandcentrix.thirtyinch.TiPresenter.sendToView(TiPresenter.java:518)
       ...

It was possible to call sendToView when the view is set but a uiThreadExecutor was not.

// attachView(view)
       ...
        mView = view;
        moveToState(State.VIEW_ATTACHED, false);
       ...

sendToView was called after mView = view; on a different Thread and before an observer was able to attach a executor

This fix introduces a new state "Running" where the view is attached and all observers are informed. Actions will be postponed until this state has been reached

passsy avatar Jul 17 '17 16:07 passsy

I don't really get the point here.

All tests are green even without your changes beside of sendToView_withView_beforeInRunningState_executesAction_without_executor. Which means (more or less)

attachView(view) {
  sendToView(view -> view.something());
}

is not "allowed" and leads to a crash. Ok - but: Why someone should do something like that? You can call directly view.something() because attachView(view) is called and it is safe to call it. I agree that it should not crash. But it is not so important to me that we introduce // TODOs in our code base...

Whatever. Please provide at least one test which leads to a crash if you call sendToView(view) "outside" of attachView(view) directly...

sendToView was called after mView = view on a different Thread and before an observer was able to attach a executor

How can this happen? As I said: Please provide one test which shows exactly this issue.

StefMa avatar Jul 18 '17 08:07 StefMa

I provided a failing test which is sendToView_withView_beforeInRunningState_executesAction_without_executor. As I said it is a multithreading problem. I have no idea how to stop the thread before line moveToState(State.VIEW_ATTACHED, false); where normally the mUiThreadExecutor will be attached.

My workaround: Don't attach a UiThreadScheduler at all and execute code in onAttachView which will be called before attachView finishes.

How to reproduce this crash: Do some kind of async work (network request), bring the app in the background. When the app opens again and attachView will be called you have to call sendToView exactly after mView = view;, right before a UiThreadScheduler is attached in the next line.

The TODO is there because it doesn't make a lot of sense to introduce a new TiPresenter.State as part of the public API right now. I'm already planning a complete rewrite of the observer API without the before/after super boolean.

passsy avatar Jul 18 '17 16:07 passsy

I provided a failing test

No. Not really. It is obviously that this test will fail. Because the TiPresenter#setUiThreadExecutor works "hand in hand" with the TiActivityDelegate. The test don't use the Delegate so it is obviously that the uiThreadExecutor is null.

I want to see a test like you describe:

Do some kind of async work (network request), bring the app in the background. When the app opens again and attachView will be called you have to call sendToView exactly after mView = view;, right before a UiThreadScheduler is attached in the next line.

Yes - maybe it is not unit testable. But we can start with implementation tests as well 👍

sendToView exactly after mView = view;, right before a UiThreadScheduler is attached in the next line

Maybe a stupid solution. But can't we just switch the mView = view with moveToState(State.VIEW_ATTACHED, false); ? 🤔

- mView = view;
- moveToState(State.VIEW_ATTACHED, false);
+ moveToState(State.VIEW_ATTACHED, false);
+ mView = view;

moveToState() only change the state and check if it's allowed. Plus it calls all observers. So maybe another solution is to extract the observers to a method like notifyObservers() and call it before mView = view?! 🤔

- mView = view;
- moveToState(State.VIEW_ATTACHED, false);
+ moveToState(State.VIEW_ATTACHED, false);
+ notifyObservers();
+ mView = view;

Additional to all: If it's just a "one in a million crash" plus you want to change the observer API anyway. Why should we rush that code in? 🤔

StefMa avatar Jul 19 '17 06:07 StefMa

@StefMa

Apparently, its not one in a million scenario anymore, however I have actually redid something about my implementation as it seems to happen in that place only where we could simply end up with literally more than 500 commit files.

I'll monitor it more with next release.

Sent from my Samsung SM-G950F using FastHub Debug

k0shk0sh avatar Jul 19 '17 07:07 k0shk0sh

I came to the conclusion I should remove the "multithreading" unit test. Unit test shouldn't mimic multi threading scenarios.

Flip mView assignment with observer notifications

Changing the mView assignment with the observer notification is not a good idea for two reasons:

  • The observer API gives you callbacks exactly before and after a lifecycle method. In this case right before/after onAttachView(view). The presenter shouldn't be in a different state (mView not set) compared to the onAttachView(view) callback.
  • Existing implementations could make use of getView() in the first callback. Therefore this fix would be a breaking change. And it doesn't have to be one.

Alternative solution

I think this boolean flag solution is a minimal as it can be. Alternatively we could synchronise attachView, detachView and sendToView on the same lock. Not sure if it's a better solution

Why rush?

It's a bug and we can easily fix it without changing any API. I don't know when the new observer feature will be ready to be shipped. Better fix it now and replace the fix with a better solution later.

passsy avatar Jul 19 '17 10:07 passsy

@passsy well, after last update it seems the issue still happening, I don't see that this will break any existing implementation for others, I hope it gets merged soon :)

Sent from my Samsung SM-G950F using FastHub Debug

k0shk0sh avatar Jul 21 '17 13:07 k0shk0sh

@passsy @StefMa well, I guess I find my mistake.

I was calling .doFinally(() -> sendToView(BaseMvp.FAView::hideProgress))); instead of .doOnComplete(() -> sendToView(BaseMvp.FAView::hideProgress)));

which caused the stream to be emitted even when the observer is disposed which I think 90% is the root cause :faceplam: will monitor this within the next release and get back on it.

k0shk0sh avatar Jul 27 '17 03:07 k0shk0sh