ember-concurrency: the solution to so many problems you never knew you had
ember-concurrency is an Ember.js Addon that strikes at the root of countless challenging, error-prone, boilerplate, and mundane aspects of writing web apps that deal with asynchrony and concurrency. And if you’re thinking “Hmm, asynchrony and concurrency? My app has a bit of that, but I doubt it’s enough that this ember-concurrency thing would apply to me,” then I encourage you to keep an open mind — this article applies to you and your app more than you might expect.
I recently had the pleasure of presenting ember-concurrency to the good folk at the Ember NYC Meetup, and the feedback has been extremely positive. I think I can summarize the average response via the following two tweets: “There is a whole category of problems in Discourse (and other Ember apps!) that ember-concurrency solves elegantly”, and “I didn’t even realize how big a problem I had until I saw [ember-concurrency]”.
If you haven’t seen the video of the presentation, I hope you’ll take the time to watch it (and check out the slides for interactive examples/demos — the slideshow itself is an Ember app that uses ember-concurrency). If you don’t have 45 minutes to spare (or a half hour at 1.5x speed!), I’ve written this article to summarize and reinforce some of the more salient points from the presentation.
So let’s get to it: what is ember-concurrency all about?
ember-concurrency gives you Structured Concurrency
The idea behind Structured Concurrency is that the lifespan of an asynchronous operation should never exceed that of its parent — if an operation is canceled, or encounters an error, any child operations that it spawned ought to be immediately canceled as well. This idea takes a page from the Structured Programming revolution of the late 60s, which admonished the use of GOTO in favor of clear control flow structures like for and while loops, as well as dividing the logic of your program into subroutines (functions). The rationale is that when your code has clear logical boundaries, it is easier to reason about and easier to maintain.
The above slide illustrates how easy it is for a callback to fire beyond the lifespan of the parent operation that created it: 1) user clicks Save, which kicks off an AJAX request, 2) user navigates away, causing the component to be destroyed, 3) the AJAX request finishes and runs the callback and tries to update the state on a now-destroyed Component. This is a common source of error in all frameworks, including Ember, React, and Angular, and the fundamental cause, in Structured Concurrency terms, is that a child lifespan (e.g. the AJAX request + callback) outlived the lifespan of its parent (e.g. the component that ended up being destroyed).
The not-much-better alternative is to implement lifecycle hooks (e.g. willDestroyElement or willUnmountComponent) to cancel any timers or operations that might run a callback beyond a parent’s lifespan, which is about as exciting and error-prone as manual memory management.
Ultimately, there are many Symptoms that you’re writing code in a language / framework that doesn’t properly enforce lifespan boundaries:
In other words, whack-a-mole:
Enforcing Boundaries requires Cancelation
Any language, framework, or library that intends to solve this problem must incorporate some notion of Cancelation into the solution: when a parent lifespan goes out of scope, it must be possible to cancel a child operation to ensure that its lifespan does not leak beyond the now-destroyed parent’s lifespan. This highlights a shortcoming in the current Promise specification: while Promises elegantly model fault-tolerant, asynchronous operations, the inability to cancel a promise makes it an unsuitable primitive for enforcing consistent boundaries in a hierarchy of asynchronous lifespans. (That said, it might not be too much longer before we have a specification on cancelable promises.)
Enter ember-concurrency Tasks
From this point on, many of the linked slides from the presentation have interactive examples/demonstrations. If some of these concepts are foreign or confusing, be sure to try out the demos by clicking the images — they’re each linked to a specific slide from the presentation.
ember-concurrency gives you a Task primitive for building asynchronous, cancelable operations. The most distinctive feature of Tasks is that you implement them using Generator Functions and the yield operator. The semantics are reasonably simple: yielding a promise from within a task’s generator function will pause execution until the promise resolves, at which point execution will resume if the promise fulfilled, or an exception will be thrown from where the yield occurred if the promise was rejected.
The ability to pause execution at a yielded promise provides two powerful benefits: 1) the task can be externally canceled at any point where there is a yield, and 2) you can write asynchronous code in a sequential manner, as if the code were running synchronously, which means you can avoid the callback heck of promises (and observables). (This second point is a major motivator behind the ES7 async/await proposal, but it’s worth pointing out that until the proposal supports cancelation, ember-concurrency can’t use async functions as task functions.)
This syntax also supports pausing execution within familiar control flow constructs like for/while loops:
It might take some getting-used-to if you’ve never seen syntax like this before, but in very little time I can guarantee that you won’t miss slicing your async operations into callbacks (although ES6 arrow functions certainly make this less painful), plus it opens the door to some remarkably powerful patterns described below.
Both of the examples above have a Cancel button that stops the task at whatever yield it is paused on. This is an example of explicit cancelation, but as mentioned earlier, the fundamental guarantee of any library that offers Structured Concurrency is that a subroutine’s lifespan is never allowed to exceed the lifespan of its parent, and the way ember-concurrency delivers on this guarantee is that Tasks are automatically canceled when the object they live on is destroyed. This is very important, and is part of the reason that ember-concurrency enables you to eliminate so much cleanup boilerplate within lifecycle hooks, and explains how, if you’re using Tasks properly, you can eliminate most if not all willDestroy, willDestroyElement hooks from your code (not to mention clearTimeout or Ember.run.cancel).
As a case in point, all of the slides from the presentation I’ve been linking to are Ember Components, and many of them feature complex examples of asynchrony, yet none of them implement lifecycle hooks like willDestroyElement because all asynchronous code is contained within ember-concurrency tasks, which are automatically canceled when the Component is unrendered (i.e., when switching between slides).
Task State, Baked-in
One of the most mundane aspects about writing robust, asynchronous UI is toggling loading spinners and stylizing buttons as an async operation starts and finishes. Usually this involves setting some isRunning state to true at the beginning of an operation, and setting it back to false in the finally hook of a promise. Fortunately, ember-concurrency Tasks are smart enough to know when they are running and expose isRunning/isIdle properties that track this state so that you don’t have to do it yourself:
What about unwanted concurrency?
Hopefully it’s becoming clear how Tasks relieve you of a great deal of boilerplate that plagues every-day async programming (regardless of whether you’re using Ember, React, Angular, Rx, Bacon, etc), but there’s one major use case we have yet to address: in all of these examples, if you rapidly click the Perform button, you’ll notice that there’s nothing stopping the task from running concurrently (e.g. running multiple instances of itself at the same time). The reason is this: by default, Tasks run with unbounded concurrency.
Sometimes, this is the desired behavior — it’s certainly the most familiar default behavior — but often, what you really want is to ensure that a task run one-at-a-time. Generally speaking, there are only 3 ways to enforce such a policy, and here’s what each of these approaches looks like when you don’t have Tasks in your arsenal:
Option 1: Ah yes, our old friend, manually-set-and-unset-isRunning-flag: if the task is already running, ignore any future attempts to perform the task again, until the currently-running task completes.
Option 2: This one is kind of clever: we can assemble our own (uncancelable) chain of promises by casting a potentially-null this.promise into a promise, and then chaining our operation off of it. The result of this is that all tasks are enqueued to run in sequence, and none are ignored or canceled.
Option 3: if there’s already a task running, cancel it, and then start a new one. Bad news: promises aren’t cancelable, so this isn’t a real solution.
Again, these are your options when you don’t have Tasks at your disposal, and they all have the following in common: 1) they are boilerplate, 2) they require clever promise acrobatics (and the restarting a promise-based operation isn’t even possible), and 3) they are extremely commonplace in every app ever.
So what does ember-concurrency have to say about all of this?
Task Modifiers to the Rescue
At this point I highly, highly recommend stepping through the slides and trying out the interactive task graphs yourself. They are extremely satisfying and illustrative to see in action.
You can apply a Task Modifier to any ember-concurrency task that you implement. Each of the following Task Modifiers enforce that only one instance of a task runs at a time, but the way they enforce this policy is what makes each of them distinct:
Option 1: the .drop() Task Modifier
Applying drop() to a task enforces that only one instance of a task run at time by ignoring all future attempts to perform the task while the current instance is still running. The graph above demonstrates how all attempts to perform the task while it is already running are “dropped” — they are canceled before they even start.
Option 2: the .enqueue() Task Modifier
Applying enqueue() forces each task to run in sequence — note how, unlike drop(), enqueue() doesn’t cancel tasks in order to enforce one-at-a-time execution.
Option 3: the .restartable() Task Modifier
This task modifier enforces one-at-a-time execution by canceling any prior instances before immediately performing a new one.
What makes Task Modifiers so nice is that 1) there is absolutely zero boilerplate — the code you write within a task function doesn’t have to concern itself with how concurrency is constrained, it can just do whatever it needs to within the safe confines of a Task Modifier — 2) there is zero cost to switch between Task Modifiers — if a client asks you to change the behavior of a button so that it restarts some operation rather than letting the first operation run to completion, you don’t have to rewrite your task function, you can simply s/drop/restartable/ and call it a day — 3) ultimately, you have the flexibility to explicitly and externally cancel a task should these Task Modifiers prove insufficient to handle some atypical use case.
You have now been introduced to 95% of ember-concurrency’s API: in short, you implement tasks using generator functions and then (optionally) choose how to constrain concurrency using a Task Modifier. That’s pretty much it.
Hopefully by this point, if you’re an Ember developer, your head is swirling with ideas for potential refactors: replacing async cleanup in lifecycle hooks with a task, getting rid of your homegrown cancelation schemes, ditching those isLoading/isRunning/isProcessing flags that you manually set and unset, deleting all your if(this.isDestroyed) checks and countless other instances of defensive programming that sully your otherwise beautiful and idiomatic Ember code. But if that hasn’t happened for you yet, here are some examples to help it sink in:
Accelerating Increment/Decrement Buttons
Canonical XHR-canceling, debounced auto-complete
This next one goes out to all the RxJS fans out there.
Throttling a shared resource (like AJAX)
Delegating Work to Multiple Child Tasks
I hope you’ll give ember-concurrency a try, and I think you’ll be happy with the results. The documentation site should be more than enough to get you started, but feel free to ping me on Twitter if you’re running into any issues, or if there is any missing or unclear documentation.