hypernova icon indicating copy to clipboard operation
hypernova copied to clipboard

[Question] Runtime loading/unloading modules

Open kpelelis opened this issue 6 years ago • 2 comments

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?

kpelelis avatar Oct 31 '18 11:10 kpelelis

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 requires (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.

agis avatar Nov 02 '18 09:11 agis

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 requireing?

kedarv avatar Jun 13 '22 21:06 kedarv