-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Conversation
There was a problem hiding this 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!
The scripts/memory tests added in PR #7661 fail in Node.js 12, because the FinalizationRegistry (a relatively new ECMAScript feature) is not defined.
a4d6810
to
668711a
Compare
// 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); |
There was a problem hiding this comment.
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!
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).
In my investigation of #7622, I found two problems that caused
InMemoryCache
objects to be retained after they were discarded by application code:If
client.stop()
is called while a request is in flight,queryInfo.stop()
will be called for eachQueryInfo
object, which successfully cancels any active cache watchers, causing the reactive variable system to release all non-weak references toclient.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 toclient.cache
inmarkResult
afterstop
has been called.The
varDep
dependency function used by the reactive variable system was indirectly retaining references toInMemoryCache
instances, which was a problem because there was only onevarDep
function, defined in the module scope ofreactiveVars.ts
(and thus never garbage collected). This PR fixes that problem by associating a separate dependency function with each cache object, using aWeakMap
(keyed bycache
) 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 aboutWeakMap
being unavailable/unusable in older JS environments, it's worth mentioning thatWeakMap
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.