Skip to content
This repository has been archived by the owner on Mar 16, 2021. It is now read-only.

Fix crash when calling sendToView while the Presenter attaches the view #107

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

passsy
Copy link
Contributor

@passsy passsy commented Jul 17, 2017

@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

It was possible to call sendToView when the view 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.

This fix checks both (view and mUiThreadExecutor) to be available before calling `runOnUiThread`.

Additionally `sendPostponedActionsToView` will now be called when setting a new  uiThreadExecutor in case a third party Presenter host has to change the uiThreadExecutor while the view is attached or attaches it after onAttachView() for some reason
@StefMa
Copy link
Contributor

StefMa commented Jul 18, 2017

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.

Copy link
Contributor

@StefMa StefMa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@passsy
Copy link
Contributor Author

passsy commented Jul 18, 2017

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.

@StefMa
Copy link
Contributor

StefMa commented Jul 19, 2017

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? 🤔

@k0shk0sh
Copy link
Contributor

k0shk0sh commented Jul 19, 2017

@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

* This is a temporary field without public getter until the presenter state and notifications
* get completely refactored
*/
private boolean mRunning = false;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add volatile, will be read from multiple threads

@passsy
Copy link
Contributor Author

passsy commented Jul 19, 2017

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.

Pascal Welsch added 2 commits July 19, 2017 14:24
Improve other tests to harden the current API agains future behavior changes
@k0shk0sh
Copy link
Contributor

@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
Copy link
Contributor

@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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Development

Successfully merging this pull request may close these issues.

None yet

3 participants