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

Prevent reactive variables from retaining otherwise unreachable InMemoryCache objects. #7661

Merged
merged 11 commits into from
Feb 8, 2021

Conversation

benjamn
Copy link
Member

@benjamn benjamn commented Feb 5, 2021

In my investigation of #7622, I found two problems that caused InMemoryCache objects to be retained after they were discarded by application code:

  1. If client.stop() is called while a request is in flight, queryInfo.stop() will be called for each QueryInfo object, which successfully cancels any active cache watchers, causing the reactive variable system to release all non-weak references to client.cache. However, when the request later comes back from the server, queryInfo.markResult is called, which needlessly revives the forgotten cache (see Reactivate reactive variables when InMemoryCache acquires first watcher. #7657) by writing the network result to the cache. This PR fixes that problem by not writing to client.cache in markResult after stop has been called.

  2. The varDep dependency function used by the reactive variable system was indirectly retaining references to InMemoryCache instances, which was a problem because there was only one varDep function, defined in the module scope of reactiveVars.ts (and thus never garbage collected). This PR fixes that problem by associating a separate dependency function with each cache object, using a WeakMap (keyed by cache) to hold that information weakly. In other words, the dependency function can now be garbage collected along with the cache object, whenever the cache object becomes unreachable.

Although the usage of WeakMap in this part of the code is not new, and we haven't heard any recent complaints about WeakMap being unavailable/unusable in older JS environments, it's worth mentioning that WeakMap is not the only option we have at our disposal. We could also associate metadata with cache objects simply by attaching the metadata as a property of the cache object, which would make the cache the owner of the data, allowing both to be garbage collected at the same time. I don't love that strategy, because it involves modifying the cache object, and potentially exposes the metadata to outside tampering, but it's an option.

Copy link
Member

@hwillson hwillson 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 great @benjamn - 👍 from me (after the test:ci stuff is worked out, which I know you're in the middle of). Thanks!

@benjamn benjamn force-pushed the 7622-reactive-var-cache-leak branch from a4d6810 to 668711a Compare February 8, 2021 20:46
Comment on lines +41 to +44
// If the registry object itself gets garbage collected before the
// callback fires, the callback might never be called:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry#notes_on_cleanup_callbacks
registries.push(registry);
Copy link
Member Author

Choose a reason for hiding this comment

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

If you're ever tempted to use a FinalizationRegistry, make sure you don't accidentally let the registry itself get garbage collected, because then any unfired callbacks you've registered will never fire!

@benjamn benjamn merged commit de53303 into main Feb 8, 2021
@benjamn benjamn deleted the 7622-reactive-var-cache-leak branch February 8, 2021 20:57
benjamn added a commit that referenced this pull request Feb 12, 2021
Thanks to @francisu for pushing back on the reasoning behind #7276:
#7276 (comment)

Thanks to #7661, we now have a system for programmatically testing garbage
collection of discarded objects, so we can actually enforce the
expectation that the whole ObservableQuery is garbage collected after the
last subscriber is removed (which tears down the ObservableQuery and
removes it from the QueryManager).
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants