Engineering

Asana on Node.js

We <3 JS

Here at Asana, we’ve bet heavily on Server-Side JavaScript (or SSJS for short) from the beginning. Our in-house framework, Luna, runs both in the browser and on the server, which allows us to write code for both parts of the app once and seamlessly sync even complex representations of data in near-realtime between the user and the server.

It’s hard to emphasize just how important embracing JavaScript on the server and client has been for us. We write application logic once, and know that it’ll be executed identically on the server and client-side. We allow users to undo many destructive actions – thanks to the client and server process each having an identical view of the data, and identical code to process or revert an operation, we can show the results to the user immediately. We can subscribe on the server to potential changes to queries, know exactly which parts of the data are needed on the client because the server knows every view state on the client, and sync the changes to the browser so you see updates to your data in near-realtime. In short, “JavaScript everywhere” is the secret sauce that makes Asana possible.

The Road to Node

At the time we chose SSJS, Node wasn’t nearly as mature as it is today. For several years now, we’ve been running on V8CGI (now officially named TeaJS), which runs on V8, the same JS virtual machine that powers Node.

Since the days when we first chose to build our service on SSJS, Node has come a long way. Today, it’s the 800-pound gorilla of SSJS frameworks, and deservedly so: it’s blazingly fast, its maintainers have cannily left many decisions to userspace rather than baked into the framework to allow faster iteration, and the community around it has (perhaps as a result) been incredibly prolific. NPM, the node package manager, boasts a repository of over 35k packages, with many more added every day. Node itself is, as of this writing, the second-most-watched repository on Github (with Bootstrap, a project that obviously enjoys a very broad audience, claiming the top spot).

Now, obviously we don’t just choose our core infrastructure by popularity contest. From using Node in side-projects, to constantly finding packages on NPM that did just what we wanted but had to be adapted to run in our environment, to finding features we knew we could build much more easily with Node’s async I/O — eventually the evidence became convincing enough that we decided to make the leap and begin porting our Luna framework to Node.

The benefits we hoped to achieve are:

  • Require() all the packages: Having a thriving package ecosystem obviously means needing to write less code internally. This is probably one of the strongest arguments in favor of the switch to Node. It’s a blessing to find there’s a great community out there building high-quality modules that often are stellar exemplars of the Single Responsibility Principle: they do one thing and do it well.
  • Standard package system: NPM packages are nicely isolated from one another – Node only supports including packages via a qualified import, in essence simply giving you a reference to a JS object back that you can bind to the name of your choice. We actually began adopting this style of module ahead of fully adopting Node.
  • Overlapping I/O: There are several features we have that would benefit greatly from the ability to do overlapping I/O. One great example is that we want to be able to allow API integrators to specify a way to create hover cards for links in Asana that reference objects in their app (if you’re familiar with oEmbed or Twitter Cards you’ll get the basic idea). We can improve the responsiveness of typeahead search.
  • Open-Source Luna: This is obviously a much longer-term goal. Working with a reactive framework relieves developers of an incredible burden, and frameworks enabling basic functional reactive programming are becoming more popular (e.g. Bacon.js or React), but Luna takes it beyond merely a way to build interfaces and considers a holistic approach to how reactivity can make writing responsive, realtime apps for the web natural and fun. Asanas are not by nature an envious people, and we’d love to be able to get the framework into the hands of others; it’d be a shame if we’re the only ones having all the fun. Moving to Node removes one more significant barrier to getting up and running with Luna, and will hopefully mean it’s more approachable to the many coders now familiar with SSJS thanks to Node. It’s not quite time to put “! curl -f https://github.com/Asana/luna” in your crontab just yet, but it’s on our long-term radar.

Making the switch

Fortunately for us, we had a few factors working in our favor:

  1. V8: We were already running V8 for the server-side. In theory, JS runtimes should all behave the same, but as the old saying goes “in theory, the difference between practice and theory is small, but in practice…” Most of the Luna framework runs on any JS runtime (it has to, since it also runs in the browser), but there are server-only components that expect to run in a given VM, so we were fortunate that Node.js and our existing V8CGI run on the same V8 virtual machine.
  2. Tests: We have an exhaustive array of tests for everything from the behavior of the UI in different browsers down to the nuts and bolts of handling HTTP requests. Doing a major refactor like this would be almost unthinkable without the ability to run tests to catch all kinds of tricky edge cases. Porting to Node.js became conceptually very simple: replace the V8CGI executable with node, attempt to run the tests, fix whatever broke, repeat until all the tests are passing.
  3. The host abstraction: Because Luna is able to run in the browser or on the server, we abstract the “host” Luna is running on. The host is responsible for doing things like making HTTP requests, running cryptographic routines, executing child processes, talking to Memcache, and so forth – in short, the functionality not provided directly by the JS runtime that differs from environment to environment. Since we had a V8CGIHost already, it was possible to simply create a new NodeHost type and put Node-specific logic there.
  4. Flexible provisioning: Thanks to putting the choice of JS runtime behind a single flag, we were able to first start some development environments running on node, then run sets of tests on node, then roll out to beta, and finally incrementally replace machines in production. The entire roll-out was able to happen without a single “flip the switch” moment, and always with the ability to easily roll back when we encountered issues.

But it wasn’t all smooth sailing:

  1. Faux amis: Sometimes, two human languages will have words that seems like it should mean the same thing, but don’t. Ask anyone who has tried to explain that they’re “embarassado” of their poor Spanish and you’ll get the idea. Node and V8CGI share this issue at times. The Buffer class behaves very similarly in the two languages, but has a few little differences. For example, in V8CGI, Buffers throw an exception if you attempt to convert to an encoding for which the Buffer contains invalid data; in Node, it will silently drop invalid data.
  2. Different approaches to concurrency: While Node has done a great deal to popularize continuation-passing style (sometimes affectionately referred to as “callback spaghetti”), the Luna codebase uses lightweight coroutines for increased clarity and programmer productivity. While we resorted to writing our own fibers module for V8CGI, Marcel Laverdet’s excellent “fibers” module for Node came to the rescue here, allowing us to provide a very similar interface.
  3. The devil’s in the details: There are too many small subtle issues to bear enumerating them all, but small tasting might suffice: a web proxy that handles requests differently when input is buffered vs. streamed, inconsistencies in consistent hashing implementations (oh the irony!), and differences in how the memcache protocol is implemented are but a few of the small little issues that are individually mere nuisances, but can be a real problem in aggregate.
  4. Node is not perfect (yet?): Once the code was running in beta, we noticed an odd phenomenon where processes would occasionally simply hang. Once we’d eliminated all sources of potential missed callbacks, we discovered that there was a bug in node itself reading a payload of precisely 65536 bytes off a pipe – a chunk size that turned out to be somewhat common for us. Fortunately the issue was resolved within days. Node may not be as battle-tested as some more mature technologies, but the responsiveness of the core team and the wider community offsets this enough that, as long as you’re willing to file the rare bug report, it’s well worth the investment.

Conclusion

As you read this, we’re currently serving 100% of our production traffic from node. The process of changing the underlying runtime for a production system can be daunting, but with comprehensive tests, flag-based configuration, and incremental roll-out, we could eliminate significant risks. Now that the groundwork is done to run on Node, we’re excited to explore ways we can exploit the advantages of our new runtime, and become more active participants in the Node community. It’s an exciting time to be a Node fan at Asana!

Would you recommend this article? Yes / No