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

Minimal changes to allow manual re-initialization of NodeJS process #131

Merged
merged 6 commits into from
Apr 5, 2024

Conversation

Tales-Carvalho
Copy link
Contributor

This PR aims to solve issue #130 by creating an interface to terminate and initialize the bridge's underlying NodeJS process and threads responsible for the communication.

To do this, I moved the calls responsible for referencing the bridge classes instances to the initialization function, refactored EventLoop and EventExecutorThread's constructors to re-initialize its properties, changed the connection logic so it does not stop upon exception or process termination, and implemented a terminate() function in __init__.py.

With this change, the following behaviour is possible:

import javascript

javascript.init()
javascript.eval_js('console.log("Hello from 1st NodeJS process!")')
javascript.terminate()

javascript.init()
javascript.eval_js('console.log("Hello from 2nd NodeJS process!")')
javascript.terminate()

Upon running this code, each time we terminate() and init(), a new NodeJS process is spawned and the bridge switches to communicate with it. Note that the first eval_js call can be done without calling init() because this function is called within the module's initialization. The function itself checks if the bridge has been previously initialized, so calling it again to ensure it is up is fine.

Most importantly, these changes should not break the current usage of the bridge. I have verified this by running test.py and test_general.py, and no exception is raised.


I have only noticed one caveat about these changes regarding the global variables exposed by the module. When running the bridge interface after re-initializing the NodeJS process at least once, the following code does NOT work:

from javascript import globalThis
# ...
print(globalThis.Date().toLocaleString())

However, this DOES work:

import javascript
# ...
print(javascript.globalThis.Date().toLocaleString())

This is because the variable we get from from javascript import globalThis can only point to the global context of the first process as that's the reference that exists during the module import. On the other hand, calling javascript.globalThis can return the updated reference after the bridge re-initializes. I suspect this is also the case for console and RegExp as they are exposed with the same logic.

A cleaner way to solve this would be to write a getter for each of these variables and expose that instead of the references to variables themselves. For example, we can write in __init__.py:

def get_global_this():
    return config.global_jsi.globalThis

This however would change the interface to the bridge (i.e., we would have to call get_global_this().Date() in the code above), therefore I did not commit it in this PR. Please let me know if this is desirable though.

@@ -163,7 +164,7 @@ def loop(self):
self.threads = [x for x in self.threads if x[2].is_alive()]

if len(self.freeable) > 40:
self.queue_payload({"r": r, "action": "free", "ffid": "", "args": self.freeable})
self.queue_payload({"r": 0, "action": "free", "ffid": "", "args": self.freeable})
Copy link
Contributor Author

Choose a reason for hiding this comment

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

About this change: currently this does not work at all because r is not defined. As this calls JS without listening to a return, the request id is ignored so passing "r": 0 works.

@extremeheat
Copy link
Owner

Thanks for working on this. As the lib was not designed around the Node.js process being stopped, all variables and all state depending on the previous node process will stop working and throw errors. Not just globalThis, but require(), and any other function currently stored on the Python side depending on a reference in JS land. This may or may not be handled by the user but if they intend to use this new API call, I think it's OK to assume the user will have written their code in a way to handle this (re-doing imports, using import javascript, etc.)

However I don't understand the removal of the error handling when the Node process is killed or terminated. This is intended to prevent later errors and other undefined behavior. If the Node process crashes this normally means the exception happened outside of a call done by Python as we handle most errors on object doing property accesses and calls. Can you explain this change?

@Tales-Carvalho
Copy link
Contributor Author

However I don't understand the removal of the error handling when the Node process is killed or terminated. This is intended to prevent later errors and other undefined behavior. If the Node process crashes this normally means the exception happened outside of a call done by Python as we handle most errors on object doing property accesses and calls. Can you explain this change?

Thank you for the review! I have initially removed the error handling because that was inadvertently terminating the new Node process. However I took a second look at it and figured out a better way to handle this by checking the current Node process status and adding calls to sendQ, as it's done with other cases. With this I restored the stop() call in the error handling. Please let me know what you think about it!

@antont
Copy link

antont commented Mar 30, 2024

Maybe this could be used in a mechanism to restart NodeJS in case it crashes or the JS app there ends up in an invalid state?

I don't think I need multiple instances, but am using the bridge in a long running Python server, to use a JS/TS lib that does websocket calls to a service. We haven't had problems, but was just thinking that maybe something like this could help if we get a need to restart the bridge / node some day.

I will be needing multiple worker processes though but I think those end up with separate Python interpreters and thus bridge instances & node processes anyhow.

@@ -20,6 +27,11 @@ def init():
init()


def terminate():
Copy link
Owner

Choose a reason for hiding this comment

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

Can you add a note for this and the other new API method to the docs along with an explanation or how to use?

@Tales-Carvalho
Copy link
Contributor Author

Maybe this could be used in a mechanism to restart NodeJS in case it crashes or the JS app there ends up in an invalid state?

Yes, this is exactly the use case for this change. Note that this does not add support for parallel NodeJS process, but it allows the process to be re-initialized.

With this change, it is now possible to wrap every (independent) JS call with javascript.init() and javascript.terminate(), so the process is always re-initialized when needed. I assume it's also possible to catch bridge exceptions and handle them with terminate() followed by init(), to restore the process for subsequent JS calls.

@Tales-Carvalho
Copy link
Contributor Author

@extremeheat I have just added two paragraphs in docs/python.md about controlling the node process and the API calls, as well as describing the caveat I mentioned in this PR. Please feel free to edit my text and to bring some of it to README.md, if you see necessary.

@extremeheat
Copy link
Owner

Thanks for the contribution! I agree with the comment about improving error handling but that can be handled outside the PR and may require some additional thought (as we'd have the same problem of existing state being unrecoverable, so something would need to be done to deal with this).

@extremeheat extremeheat merged commit adab7cf into extremeheat:master Apr 5, 2024
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants