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

Headless Tasks on react-native 0.76.3 #2225

Open
christocracy opened this issue Dec 5, 2024 · 15 comments
Open

Headless Tasks on react-native 0.76.3 #2225

christocracy opened this issue Dec 5, 2024 · 15 comments
Labels
Android:HeadlessTasks ReactNative:NewArch Issues related to RN New "Bridgeless" Architecture

Comments

@christocracy
Copy link
Member

christocracy commented Dec 5, 2024

@mikehardy related to facebook/react-native#48124

I had a good look at the code involved with RN headless-tasks and added a note in my own wrapper

RN headless-tasks have no mechanism for the Javascript to signal completion of their tasks: you can either configure a timeout for your tasks (letting each task time-out, scheduling a timeout Runnable or provide no timeout at all, which will cause a memory leak in RN's HeadlessJsContext in these two collections

I prefer not to execute and -- let time-out -- a Runnable for each and every invoked headless-task.

Without providing a timeout for your headless-tasks, those collections in HeadlessJsTaskContext will continue to grow and never be cleared. The only place where elements seem to be removed from those two of its collections (mActiveTasks and mActiveTaskConfigs) is in finishTask. finishTask is called only from timeouts.

A major problem I had was that you only know the RN taskid until after startTask has already executed, preventing me from being able to transmit that into the user's Javascript headlessTask.

So I had to make my own wrapper around RN tasks and manage my own collection to keep a taskId mapping between my tasks and RN's.

The whole purpose of my headless-task runner is to be able to use a javascript method .finishHeadlessTask(taskId) for users to signal completion of their work.

What would be nice is if RN provided an internal way to signal completion of headless-tasks.

const bgGeoHeadlessTask = async (event) => {
  const params     = event.params; // <-- our event-data from the BG Geo SDK.
  const eventName  = event.name;
  const taskId     = event.taskId; // <-- very important!
  
  console.log(`[BGGeoHeadlessTask] ${eventName}`, JSON.stringify(params));
  // You MUST await your work before signalling completion of your task.
  await doWork(eventName);
  
  // Signal completion of our RN HeadlessTask.
  BackgroundGeolocation.finishHeadlessTask(event);
}

BackgroundGeolocation.registerHeadlessTask(bgGeoHeadlessTask);

///
/// A stupid little "long running task" simulator for headless-tasks.
/// Uses a simple JS setTimeout timer to simulate work.
/// 
let doWorkCounter = 0;

const doWork = async (eventName) => {
  return new Promise(async (resolve, reject) => {
      doWorkCounter = 0;
      // Perform a weird action (for testing) with an interval timer

      // Print * tick * to log every second.
      const timer = setInterval(() => {
        console.log(`[BGGeoHeadlessTask][doWork] * tick ${++doWorkCounter}/10 *`);
      }, 1000);
      // After 10s, stop the interval and stop our background-task.
      setTimeout(() => {
        clearInterval(timer);
        resolve();
      }, 10000);
    }
  });
}
@mikehardy
Copy link
Contributor

Are you sure this statement is true?

RN headless-tasks have no mechanism for the Javascript to signal completion of their tasks

I think when the javascript async task resolves its Promise, that finishTask is called

I see calls to this from AppRegistry.js's code through the CodeGen-d imagine-it-exists layer to here

https://github.com/facebook/react-native/blob/e7b9d70e0a134d117c77b87101684252b10f149d/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/HeadlessJsTaskSupportModule.kt#L39

But, prove me wrong :-) (I could easily be wrong here, I'm reading this code first time)

@christocracy
Copy link
Member Author

christocracy commented Dec 5, 2024

Thanks, that file is new to me; I'll have a close look.

It seems to me that pre-new-arch headless-tasks used to stop after executing the last line of code in the function -- you had to be careful to await your work.

here's a simple headless-task that awaits no Promise. It runs a setInterval to log * tick * every second. I launch a WorkManager job (.startBackgroundTask() to keep the app alive while Javascript counts the seconds.

My headless-tasks are configured to have a timeout of 120s and this code keeps ticking until the timeout.

const bgGeoHeadlessTask = async (event) => {  
  console.log('[BGGeoHeadlessTask]');
    
  doWorkCounter = 0;
  // Perform a weird action (for testing) with an interval timer and .startBackgroundTask.
  const bgTaskId = await BackgroundGeolocation.startBackgroundTask();
  // Print * tick * to log every second.
  const timer = setInterval(() => {
    `[BGGeoHeadlessTask][doWork] * tick ${++doWorkCounter} *`);
  }, 1000);

  // After a reallllllly long time, stop the interval and stop our background-task.
  setTimeout(() => {
    clearInterval(timer);
    BackgroundGeolocation.stopBackgroundTask(bgTaskId);
  }, 2000000); // <-- keep counting seconds for a loooong time.
}
.
.
.
12-05 18:38:50.611  8126  8164 I ReactNativeJS: [BGGeoHeadlessTask][doWork] * tick 115 *
12-05 18:38:51.620  8126  8164 I ReactNativeJS: [BGGeoHeadlessTask][doWork] * tick 116 *
12-05 18:38:52.625  8126  8164 I ReactNativeJS: [BGGeoHeadlessTask][doWork] * tick 117 *
12-05 18:38:53.632  8126  8164 I ReactNativeJS: [BGGeoHeadlessTask][doWork] * tick 118 *
12-05 18:38:54.641  8126  8164 I ReactNativeJS: [BGGeoHeadlessTask][doWork] * tick 119 *
12-05 18:38:54.729  8126  8126 D TSLocationManager: [onHeadlessJsTaskFinish] taskId: 1

@christocracy
Copy link
Member Author

christocracy commented Dec 6, 2024

This should be where the headless JS-task is executed.

IMG_2577

I wonder what this NativeHeadlessJsTaskSupport is all about?

@mikehardy
Copy link
Contributor

I think it is the missing link between the javascript and the direct binding to the native module(s), and that as the Promise on JS side resolves, it calls the native TurboModule's listener which is this support object, notifyTaskFinished method, which closes the loop on clearing out things related to that taskId and shutting down etc without waiting for some un-/ill-defined timeout.

@christocracy
Copy link
Member Author

christocracy commented Dec 6, 2024

Oh crap...I put some console.log in the source for AppRegistry.js for that block above:

NativeHeadlessJsTaskSupport is evaluating null. The missing link is missing :)

Something I'm doing (or not doing) is causing that to be missing. I wonder what's the signal that causes that to exist?

taskProvider()(data)
      .then(() => {        
        console.log('*** taskProvider then()', NativeHeadlessJsTaskSupport);
        if (NativeHeadlessJsTaskSupport) {
          console.log('*** IN IF');
          NativeHeadlessJsTaskSupport.notifyTaskFinished(taskId);
        } else {
          console.log('*** IN ELSE');  // <--- Always ending up here.
        }
      })

@mikehardy
Copy link
Contributor

Out of current spelunking time box on this but near as I can tell by clicking around the symbol search on github UI

  • the TurboModule for NativeHeadlessJsTaskSupport has a JS spec and a kt class, and it is in the core module list / "basic module information provider map" on the native side such that you can always ask the TurboModule system to get the module and it should create one if needed and hand it over

  • simply importing the JS-level NativeHeadlessJsTaskSupport should cause that to happen by that file exporting a symbol that is the result of a TurboModule .get call for that TurboModule spec. But it's a .get not a .getEnforcing so it may silently fail and return null on JS side indicating there is no native TurboModule matching the spec (for some reason?)

  • AppRegistry is supposed to get NativeHeadlessJsTaskSupport (with valid backing native TurboModule) via that import and use it to call notifyTaskFinished at JS level which will call it at native level and do all the things we want by closing the loop there, signal-wise, but since it's null it does not

So why is the TurboModuleRegistry .get here not returning one?
https://github.com/facebook/react-native/blob/c54ba09d2e5777a3f238caaf181299f4c6910097/packages/react-native/src/private/specs/modules/NativeHeadlessJsTaskSupport.js#L20

Especially when it seems to be peppered in to all the correct spots in the list of basic packages?
https://github.com/facebook/react-native/blob/c54ba09d2e5777a3f238caaf181299f4c6910097/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java#L157

Very curious

@christocracy
Copy link
Member Author

I changed to TurboModuleRegistry.getEnforcing and get a clue.

Invariant Violation: TurboModuleRegistry.getEnforcing(...): 'HeadlessJsTaskSupport' could not be found. Verify that a module by this name is registered in the native binary.

@mikehardy
Copy link
Contributor

That's expected based on your observed behavior but unexpected based on reasonable expectations 😆 - why...what, in your use case + in the React init process is not happening such that the TurboModuleRegistry isn't populated with that or is not returning that object?

@christocracy
Copy link
Member Author

christocracy commented Dec 7, 2024

Damn, I'll have to keep my wrapper until I figure out how these new TurboModules are created and composed.

Fortunately, I've always used a wrapper around AppRegistry.registerHeadlessTask:
BackgroundGeolocation.registerHeadlessTask, so I can just copy what RN's headless-task executor does to automatically and transparently finish-tasks after the user's function resolves.

Once I figure out what's wrong, I can simply remove it all with no change to my API and nobody will ever know.

Thanks for the tips!

@mikehardy
Copy link
Contributor

Wish my tips led to effective + understandable resolution. All this stuff is vital to notifee and react-native-firebase so I'm trying my best to understand it as well (vs just expecting it to work...). I think we're both about to learn something then, just don't know what. And I need to do some more testing on the RNFB/Notifee side to make sure all the things you've already mentioned are being done correctly, I think it's a list of about 5-6 things right now and I bet I'm handling 1-2 correctly. Adding to that list of skills either way ("running react-native built from source" is on there now, "command-line trigger of headlessJS" is on there, etc)

@christocracy christocracy added ReactNative:NewArch Issues related to RN New "Bridgeless" Architecture Android:HeadlessTasks labels Dec 7, 2024
@superyarik
Copy link

can i get such errors because of this topic?

RuntimeException
Could not invoke HeadlessJsTaskSupport.notifyTaskFinished

found it in sentry logs

Copy link

This issue is stale because it has been open for 30 days with no activity.

@github-actions github-actions bot added the stale label Jan 31, 2025
@tsachit
Copy link

tsachit commented Jan 31, 2025

can i get such errors because of this topic?

RuntimeException
Could not invoke HeadlessJsTaskSupport.notifyTaskFinished

found it in sentry logs

Found any solution to this?

@github-actions github-actions bot removed the stale label Feb 1, 2025
@brunezkey
Copy link

can i get such errors because of this topic?

RuntimeException
Could not invoke HeadlessJsTaskSupport.notifyTaskFinished

found it in sentry logs

We're facing this issue as well. Created an issue to request assistance.

@abhayagrawal-fareye
Copy link

can i get such errors because of this topic?

RuntimeException
Could not invoke HeadlessJsTaskSupport.notifyTaskFinished

found it in sentry logs

Facing same issue with react-native 0.73.9 and "react-native-background-geolocation": "4.18.3".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Android:HeadlessTasks ReactNative:NewArch Issues related to RN New "Bridgeless" Architecture
Projects
None yet
Development

No branches or pull requests

6 participants