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

Server side hot reload #809

Merged
merged 10 commits into from
Jan 12, 2017
Merged

Conversation

drewpc
Copy link
Collaborator

@drewpc drewpc commented Jan 7, 2017

This patch enables server-side hot module reload on the existing master branch. Note: this does not include any Webpack changes nor does it include any changes to ExpressJS unless required for HMR to work. It simply reloads the existing changes when Webpack successfully recompiles the client code. This is a refactor of the code from #791 to only include server side HMR changes. Discussed in #773.

UPDATE: this branch was rebased and changes from #808 were removed. It is now a standalone patch.

server.use((req, res, next) => {
reactServer.middleware(req, res, next, require(serverRoutes));
});
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like there's an extra level of indentation here?

Surprised eslint didn't catch that. 😕

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed. I don't think that eslint is working properly. asini run lint blows up and there's no script in the root level package.json to run lint. I'll look into it.

Copy link
Contributor

Choose a reason for hiding this comment

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

🙀

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

(at least in my setup it's blowing up)

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmmm... we just run asini run test in CI. So each package is responsible for making sure that linting is part of its test script. It looks like the CLI's package.json should be running tests as part of the gulp test phase of its test script.

Wonder where that's breaking... 🤔

Copy link
Collaborator Author

@drewpc drewpc Jan 10, 2017

Choose a reason for hiding this comment

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

so...lint doesn't exist anywhere else. There's a weird mix of gulp tasks and package.json run scripts though. And it really bothers me that they're not named the same. In some packages, it's gulp build in others it's gulp, and in other cases, you have to do npm run build.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, nice catch on the missing dep. Yeah, some modules (like react-server-cli) use the eslint gulp plugin instead of the executable. It's all kind of a weird mishmash right now. 😖

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'll clean it up and submit a PR. I already have the previous issue fixed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

PR #818

@@ -324,8 +336,7 @@ module.exports = {
const routesContent = routesOutput.join("");
// make a unique file name so that when it is required, there are no collisions
// in the module loader between different invocations.
const routesMD5 = crypto.createHash('md5').update(routesContent).digest("hex");
const routesFilePath = `${workingDirAbsolute}/routes_${isClient ? "client" : "server_" + routesMD5}.js`;
const routesFilePath = `${workingDirAbsolute}/routes_${isClient ? "client" : "server"}.js`;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice to get rid of this.

Object.keys(require.cache)
.filter((key) => /(__clientTemp|test-temp)/.test(key))
.forEach((key) => delete require.cache[key]);

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, interesting. Glad you caught this... could have been a real source of frustration. 😬

@gigabo
Copy link
Contributor

gigabo commented Jan 10, 2017

This is a cool solution. Thanks for working it out @drewpc!

LGTM 👍

@gigabo gigabo added the enhancement New functionality. label Jan 10, 2017
@jhnns
Copy link
Contributor

jhnns commented Jan 10, 2017

Thanks for splitting up the pull-request @drewpc. This makes reviewing it a lot easier 👍

The solution is very simple and works as long as you stay within the page lifecycle. However, this solution will probably leak memory over time in more advanced use-cases, since cleaning the require.cache is not a guarantee that all references have been removed. Especially if you do side-effecty things like opening connections (and keeping them open), this will become a problem because the connection won't be closed in this case.

To me, a cleaner solution is to have a separate application server process that receives a KILL signal when a new bundle has been generated. This will make sure that the server cleans up everything before it is restarted. A downside of this approach is that restarting a process takes longer than cleaning the require.cache. In my project setup, the server restart takes about 1s.

@drewpc
Copy link
Collaborator Author

drewpc commented Jan 10, 2017

@jhnns Great point! I hadn't looked deeply into this until you mentioned it. I did some basic testing on memory leaks with regard to deleting require.cache entries (modifying this code) and came to the following conclusion: my server side patch HMR solution doesn't do anything good or bad to resolve those memory leaks. In other words, if memory leaks exist in the code being required delete require.cache[<module>] doesn't "fix" that. Your solution of restarting a separate process would fix the symptom, but not the problem: memory leaks still exist.

The topic of memory leaks would be a great documentation item: patterns to avoid, ways to detect memory leaks, etc. The page cycle of react-server would be the most vulnerable, especially since it is called for each HTTP request. I'll have to look into this, but I see it as a different problem than server side HMR.

@roblg roblg mentioned this pull request Jan 10, 2017
@roblg
Copy link
Member

roblg commented Jan 10, 2017

Added breaking-change to this, due the moving of the express middleware and express-state setup out of renderMiddleware. (Which is a change that has been overdue. Thanks!)

@jhnns
Copy link
Contributor

jhnns commented Jan 11, 2017

my server side patch HMR solution doesn't do anything good or bad to resolve those memory leaks. In other words, if memory leaks exist in the code being required delete require.cache[] doesn't "fix" that.

Of course, you're right. These memory leaks exists in the code because you don't write tear-down code if you don't need it. But usually you don't just delete require.cache, so it's not an actual memory leak in production because the module is not replaced in production. It just becomes a (potential) memory leak when the cache is deleted.

But besides the memory leak I rather worry about the unknown application state after modules have been swapped out without actually tearing down everything. Your application may behave strangely in development after a while (because of stale connections, etc.). I'd rather prefer to restart the server cleanly than having to deal with an unknown application state. That's why create-react-app is also using HMR just for CSS, because CSS can be replaced without introducing side-effects.

I've explained some details of HMR in this SO answer.

drewpc added a commit to SoprisApps/react-server that referenced this pull request Jan 11, 2017
…er serverSideHotModuleReload function from PR redfin#809 so that things are more in sync.
@doug-wade doug-wade mentioned this pull request Jan 11, 2017
@drewpc drewpc merged commit c824631 into redfin:master Jan 12, 2017
@drewpc drewpc deleted the patch-server-side-hot-reload branch January 12, 2017 02:26
@drewpc drewpc modified the milestone: 0.6.0 Jan 27, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New functionality.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants