airbnb / hypernova

A service for server-side rendering your JavaScript views
MIT License
5.82k stars 207 forks source link

[Question] Runtime loading/unloading modules #144

Open kpelelis opened 5 years ago

kpelelis commented 5 years ago

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?

agis commented 5 years ago

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:

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.

kedarv commented 2 years ago

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?