Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Android headless JS timeout #33044

Closed

Conversation

marcesengel
Copy link
Contributor

@marcesengel marcesengel commented Feb 4, 2022

Summary

Fixes #33043 and thereby invertase/react-native-firebase#3955.

The issue arised because when there currently is no available React context, HeadlessJsTaskService will create a new one in background and start the task using onReactContextInitialized of ReactInstanceManager.addReactInstanceEventListener.

protected void startTask(final HeadlessJsTaskConfig taskConfig) {
UiThreadUtil.assertOnUiThread();
acquireWakeLockNow(this);
final ReactInstanceManager reactInstanceManager =
getReactNativeHost().getReactInstanceManager();
ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
if (reactContext == null) {
reactInstanceManager.addReactInstanceEventListener(
new ReactInstanceEventListener() {
@Override
public void onReactContextInitialized(ReactContext reactContext) {
invokeStartTask(reactContext, taskConfig);
reactInstanceManager.removeReactInstanceEventListener(this);
}
});
reactInstanceManager.createReactContextInBackground();
} else {
invokeStartTask(reactContext, taskConfig);
}
}

The TimingModule however is initialized asynchronously, meaning the headless JS is started before its initialization. That's an issue because the TimingModule is only run when there is JS code executing (meaning if the application is running or there is a headless task running) - this is checked by registering a HeadlessJsTaskEventListener on the HeadlessJsTaskContext in TimingModule.initialize().
@Override
public void initialize() {
getReactApplicationContext().addLifecycleEventListener(this);
HeadlessJsTaskContext headlessJsTaskContext =
HeadlessJsTaskContext.getInstance(getReactApplicationContext());
headlessJsTaskContext.addTaskEventListener(this);
}

However this event listener is never invoked because the task was started before TimingModule.initialize() is called -> TimingModule.onHeadlessJsTaskStart(...) is not called and the timer never resumes.

In order to fix this we can just invoke HeadlessJsTaskEventListener.onHeadlessJsTaskStart(...) for all currently running tasks when a new listener is added to HeadlessJsTaskContext. This call then needs to be synchronized as otherwise there's a race condition with HeadlessJsTaskContext.finishTask(...) where onHeadlessJsTaskFinish(...) could be called before onHeadlessJsTaskStart(...). See the diff for the exact changes.

Changelog

[Android] [Fix] - Fixed TimingModule related functions for headless JS tasks, eg. setTimeout

Test Plan

I did a local build with the changes and tested the provided example code from #33043 there.

@facebook-github-bot facebook-github-bot added CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. labels Feb 4, 2022
@react-native-bot react-native-bot added the Platform: Android Android applications. label Feb 4, 2022
Copy link
Contributor

@mikehardy mikehardy left a comment

Choose a reason for hiding this comment

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

This looks good to me, from my experience as a java / android native dev but - importantly - without a lot of experience in the headless JS area of react-native. Nice one @marcesengel

@marcesengel
Copy link
Contributor Author

Thank you very much for the review and the kind words @mikehardy! Do you have any insights on how to get this merged? I currently have 3 open PRs going with the oldest being from over 3 weeks ago and no feedback yet 😕

@mikehardy
Copy link
Contributor

No special tricks other than making some noise, metaphorically. You can use git commit history to find last committer (or recent frequent committer) for the file and tag them. I can try to get attention from the release crew. Can you add a link to your other 2 PRs here?

@marcesengel
Copy link
Contributor Author

Thank you very much, will do. The PRs in question are #32889/#33045 (the same changes, the original one has a comment and points to 0.66-stable and the second one points to main, as the first one didn't receive the tag "shared with react-native team") and #32863.

Thanks again for your help, much appreciated 👍

@facebook-github-bot
Copy link
Contributor

@dmitryrykun has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

@jspizziri
Copy link

Would love to see this get fixed.

@react-native-bot
Copy link
Collaborator

This pull request was successfully merged by @marcesengel in dac56ce.

When will my fix make it into a release? | Upcoming Releases

@react-native-bot react-native-bot added the Merged This PR has been merged. label Apr 11, 2022
Saadnajmi pushed a commit to Saadnajmi/react-native-macos that referenced this pull request Jan 15, 2023
Summary:
Fixes facebook#33043 and thereby invertase/react-native-firebase#3955.

The issue arised because when there currently is no available React context, `HeadlessJsTaskService` will create a new one in background and start the task using `onReactContextInitialized` of `ReactInstanceManager.addReactInstanceEventListener`.
https://github.com/facebook/react-native/blob/7ef14af81f3a1532ca1a703da666ea2e5a70a265/ReactAndroid/src/main/java/com/facebook/react/HeadlessJsTaskService.java#L94-L113
The `TimingModule` however is initialized asynchronously, meaning the headless JS is started before its initialization. That's an issue because the `TimingModule` is only run when there is JS code executing (meaning if the application is running or there is a headless task running) - this is checked by registering a `HeadlessJsTaskEventListener` on the `HeadlessJsTaskContext` in `TimingModule.initialize()`.
https://github.com/facebook/react-native/blob/7ef14af81f3a1532ca1a703da666ea2e5a70a265/ReactAndroid/src/main/java/com/facebook/react/modules/core/TimingModule.java#L69-L75
However this event listener is never invoked because the task was started before `TimingModule.initialize()` is called -> `TimingModule.onHeadlessJsTaskStart(...)` is not called and the timer never resumes.

In order to fix this we can just invoke `HeadlessJsTaskEventListener.onHeadlessJsTaskStart(...)` for all currently running tasks when a new listener is added to `HeadlessJsTaskContext`. This call then needs to be `synchronized` as otherwise there's a race condition with `HeadlessJsTaskContext.finishTask(...)` where `onHeadlessJsTaskFinish(...)` could be called before `onHeadlessJsTaskStart(...)`. See the diff for the exact changes.

## Changelog

<!-- Help reviewers and the release process by writing your own changelog entry. For an example, see:
https://github.com/facebook/react-native/wiki/Changelog
-->

[Android] [Fix] - Fixed `TimingModule` related functions for headless JS tasks, eg. `setTimeout`

Pull Request resolved: facebook#33044

Test Plan: I did a local build with the changes and tested the provided example code from facebook#33043 there.

Reviewed By: sshic

Differential Revision: D34006573

Pulled By: dmitryrykun

fbshipit-source-id: d6a821bbd6476ba278c1d8895edb4a0ba16d889e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Merged This PR has been merged. Platform: Android Android applications. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Android headless JS timeouts not working
5 participants