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

feat(core): allow unregistering callback through on #11710

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from

Conversation

arturovt
Copy link
Contributor

@arturovt arturovt commented Apr 22, 2024

This commit updates the return signature of the client's on function to be a void function,
which, when executed, unregisters a callback. This adjustment is necessary for managing instances
where objects are created and destroyed, ensuring that callbacks are properly unregistered to
prevent self-referencing in callback closures and facilitate proper garbage collection.

Typically, changing a type from void to () => void (or VoidFunction) shouldn't be considered
a breaking change because void signifies the absence of a return value, implying that the return
value of the on function should never be used by consumers.

Opting for the on approach, which returns a cleanup function, "seems" simpler because having another
function called off requires saving the callback reference for later removal. With our pattern,
we encapsulate both the registration and removal of event listeners within a single function call.

@mydea
Copy link
Member

mydea commented Apr 22, 2024

While I can see where this is coming from, so far we decided against this to reduce complexity there. We generally do not do a lot of cleanup in a lot of places because (esp. in browser) because we make assumptions about the environment where something runs. I am not an Angular expect (cc @Lms24 ), but shouldn't the goal be to rather avoid the error handler to be continuously torn down and re-created? To me, it seems like this is the optimization that should be done, not to cleanup the one lone handler that is registered there.

Now, I don't have a problem per-se to add this change, the main reason we do not have cleanup everywhere is because it adds bundle size (not necessarily this change itself, but ensuring everywhere that stuff is properly cleaned up can sum up to a not-so-small amount of code, sadly). Not saying we shouldn't do this, just thinking out loud... 🤔

@arturovt
Copy link
Contributor Author

arturovt commented Apr 22, 2024

Hey @mydea

Well, it's generally considered good practice to have a cleanup functional API, akin to addEventListener and removeEventListener. Initially, I pondered adding an off function instead of modifying on. However, as you pointed out, this approach seems to introduce additional complexity compared to enhancing the existing on function to return a cleanup function directly tied to the added callback. This approach avoids a redundant "abstraction" where a callback needs to be stored in a variable for later removal, as is the case with removeEventListener, which requires a reference to the callback.

What you mentioned about the Angular error handler also applies to other frameworks that incorporate server-side rendering and could potentially extend to microfrontend applications. Take, for example, a React application that is rendered on the server side and destroyed with each HTTP request. Similarly, in the context of microfrontend applications, consider an app associated with a specific URL segment, such as /blog. When navigating away from that URL, the corresponding app instance needs to be properly destroyed and all of the resources should be cleaned up.

@Lms24
Copy link
Member

Lms24 commented Apr 22, 2024

Hey @arturovt thanks for opening this PR!

Regarding the unsubscription mechanism we need to discuss this more thoroughly to decide what we're doing here. I'd tend to agree with @mydea that ideally we shouldn't add the extra bytes for correctly handling unsbscribing if at all possible. However, we should of course find a solution that doesn't cause hundreds of subscriptions from the errorHandler (or in other situations as you pointed out).

Consider, for instance, a scenario in Angular where the error handler is created and destroyed with each app rendering cycle.

Is this really default behaviour in Angular? If so there's probably a good reason for it but on first glance it's a bit unintuitive/unnecessary to me 🤔 Would appreciate any context/knowledge around this, thx :)

Also a general question: Is there a concrete reason for opening the PR? For example, did the SDK cause some kind of out of memory error that can be attributed to the client.on hooks? I just want to assess impact and priority for finding a good path forward.

@arturovt
Copy link
Contributor Author

arturovt commented Apr 23, 2024

@Lms24 This solution, lacking the functionality to remove an event listener when certain components are destroyed, such as any class with 'disposable' functionality, may inadvertently lead to memory leaks. When a callback captures local variables, there's no issue of leaks. However, if a callback captures this, it creates a closure that strongly retains a reference to this, preventing its garbage collection.

Describing the technical necessity behind why this is crucial can be somewhat challenging, especially when explaining the importance of removeEventListener.

Considering an Angular example: when the root injector is destroyed, it also destroyes all injectees it maintains as records. You may observe a console.error coming from ngOnDestroy (hook which is invoked when the root injector is destroyed, I added it intentionally). And at the end, you may also observe the error handler has not been GCd because its reference is retained in client hooks:

Screenshot from 2024-04-23 13-38-46

Copy link
Member

@Lms24 Lms24 left a comment

Choose a reason for hiding this comment

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

We talked about this internally and decided to go ahead with this change. Thanks for explaining and of course for opening the PR! You're right, we should be good citizens and provide a cleanup functionality. Also obviously, use it in situations like the Angular error handler.

Before we can merge this, would you mind adding tests for the unregistering functionality?

@@ -176,52 +176,52 @@ export interface Client<O extends ClientOptions = ClientOptions> {
* Register a callback for whenever a span is started.
* Receives the span as argument.
*/
on(hook: 'spanStart', callback: (span: Span) => void): void;
on(hook: 'spanStart', callback: (span: Span) => void): () => void;
Copy link
Member

Choose a reason for hiding this comment

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

Let's add a comment to the JSDocs about what's returned from the methods

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I addressed your comments, I see there're some random failures happening between commits, not sure if they're related...

This commit updates the return signature of the client's `on` function to be a void function,
which, when executed, unregisters a callback. This adjustment is necessary for managing instances
where objects are created and destroyed, ensuring that callbacks are properly unregistered to
prevent self-referencing in callback closures and facilitate proper garbage collection.

Typically, changing a type from `void` to `() => void` (or `VoidFunction`) shouldn't be considered
a breaking change because `void` signifies the absence of a return value, implying that the return
value of the `on` function should never be used by consumers.

Opting for the `on` approach, which returns a cleanup function, "seems" simpler because having another
function called `off` requires saving the callback reference for later removal. With our pattern,
we encapsulate both the registration and removal of event listeners within a single function call.
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.

None yet

3 participants