hypernova
hypernova copied to clipboard
[Question] Runtime loading/unloading modules
We have been experimenting with hypernova for our SSR architecture. A problem arose when we wanted to add or remove a module in the getComponent
method on runtime. Is there a suggested way that we could do this without restarting the server?
Specifically, we want to deploy hypernova in such a way that it supports multiple versions (ie. releases) of our SSR code, without having to restart it. However, we've noticed memory leaks.
The scheme is the following:
- we keep a map with the known versions in hypernova.js
- when a client requests a version, hypernova goes to the fs and loads the respective files
- the version is kept in the map
- if a client requests the same version, the contents are already present in the map so there's no touching the fs, otherwise, the version is searched in the fs and loaded
- if a version is not touched for some amount of time, it's deleted from the map
The SSR code is the following:
// components.js
const renderReact = require('hypernova-react').renderReact;
const MyComponent = require('./MyComponent');
module.exports = {
MyComponent: renderReact('MyComponent', MyComponent),
};
and in hypernova.js
we do the following:
// hypernova.js
// ...
function loadCodeSeparateContext(path) {
return hypernova.createGetComponent({
"0": path
})("0");
}
const getComponent = async (name) => {
var start = process.hrtime();
const [componentName, release] = name.split('.');
if (!componentName || !release) {
return null;
}
if (!componentsPerRelease[release]) {
var componentsPath = path.resolve(path.join('./testdata', release, 'components.js'));
if (await fs.pathExists(componentsPath)){
var releaseComponents = loadCodeSeparateContext(componentsPath);
componentsPerRelease[release] = {components: releaseComponents};
}
else {
return null;
}
}
componentsPerRelease[release].lastAccessed = start;
if (!componentsPerRelease[release].components[componentName]) {
return null;
}
var [seconds, nanos] = process.hrtime(start);
return componentsPerRelease[release].components[componentName];
};
function unloadReleases(expirySeconds) {
Object.entries(componentsPerRelease).forEach(([releaseId, release]) => {
if (release.lastAccessed) {
var seconds = process.hrtime(release.lastAccessed)[0];
if (seconds > expirySeconds) {
delete componentsPerRelease[releaseId];
console.log(`Unloaded release ${releaseId}`);
}
}
})
}
setInterval(() => unloadReleases(5), 5000);
hypernova({
devMode: true,
getComponent: getComponent,
port: 3030,
});
We run load tests with a total of 100 releases. The tests are made so that they keep a rolling window of 5 versions that moves by 1 version forward every ~5seconds. This means that hypernova unloads (ie. deletes from map) an old version and loads a new one every ~5secs. Unfortunately, we noticed that the instance is leaking memory at a constant rate. After some profiling, it seems that the code of a release (react etc.) is never reclaimed by the collector, since Module.cache
or something in there keeps references to it.
However, after bundling all the SSR code using webpack, so as to avoid the require
s (which seem to be the culprit), we noticed the leaks were gone and the memory stopped increasing.
I'm not sure if this is a supported use-case, so we'd like your advice here. Is that something hypernova supports (ie. multiple versions in an instance) and what is the suggested way to do this?
Thanks in advance.
Did you ever figure out why memory was growing when loading new bundles in? Can you expand more on how you used Webpack to avoid require
ing?