Clearing Async Nests with ember-concurrency

I’m contractually obligated to include a bird reference in the title.

Max Fierke
Jul 31, 2018 · 9 min read

At Iora Health, our mission is to restore humanity to health care. One of the ways we serve our mission is by building a collaborative care platform centered around caring for our patients, rather than using traditional enterprise health systems that are built around administrative tasks like billing and coding. Because our namesake is a bird and because we like fun names, we call our platform Chirp.

Chirp serves many functions. It’s an electronic health records system. It’s a scheduling platform. It’s a practice management platform. It even facilitates some billing and coding processes (gasp!). Chirp is complex.

Today we’re focused on how ember-concurrency helps manage complexity and allows us to stay focused on building features that serve our mission and help our teams care for patients.

Life before ember-concurrency

Poor concurrency control was at the heart of Charlie’s problems.

The year was 2016. Half the team was focused on building a scheduling application for our practices in Ember. There was data flying everywhere! Schedules! Calendar events! Staff members! We were stuck wrangling Promises. I wrote .then so many times, I wore out the letter t on two keyboards. It was a nightmare.

Like many Ember users, we use Ember-Data for our data loading and management needs, so a lot of our code looked something like this:

This is a snippet of our appointment-form component, circa 2016. You’ll notice that besides doing a save of the new appointment record we’ve created, we’re also updating a few bits of state on the component: isAppointmentSaving , appointmentFormErrors , and some properties on our calendar service.

In the template for the component, we used isAppointmentSaving for controlling form UI state. First, it would disable the submit button to prevent duplicate submissions. Then, it would control the display of a loading state on the appointment form, so the user knew we were altering time and space. Likewise, with the appointmentFormErrors , we managed and cleared them out manually.

Unfortunately, the manual form state tracking was error prone. What would happen if we forgot to reset isAppointmentSaving in the error state? What happens if they close the form or navigate away while it’s saving? The answer: bad things. Users could get stuck in the form, with the spinner spinning away until they gave up and reloaded the page. If a user navigated away, we’d get a nice big stack trace in our error reporting tool. We couldn’t do anything about it, and it simply told us “the component is gone.”

What is ember-concurrency?

ember-concurrency is a popular add-on for Ember, which provides a small but powerful set of tools for managing asynchronous work.

ember-concurrency provides a task-based approach for managing asynchronous work. At a high-level, it makes “Cancel” buttons, debouncing, throttling, secondary data loading, and other complicated things uncomplicated to implement. A Task is a Promise-like construct which gives you access to execution state, such as whether the task is running, is idle, or has failed and allows for setting constraints on how many instances of the task can be running. Manually tracking wasSubmitButtonClicked or isLoading and hoping that all bases are covered is no longer required. All your base are belong to ember-concurrency now. Also, unlike Promises, they can be broken, e.g. partyTask.cancel("sorry, just crashed on my couch. #Netflix") .

ember-concurrency tasks are also aware of the lifecycle of objects to which they’re attached. For tasks on a component, this means the tasks know when the component is removed from the page and will cancel themselves. This ensures the asynchronous work you have running doesn’t blow-up when it can no longer access a component that is been destroyed. If you need to make a financial argument, experts suggest that this facet alone will save you an estimated $100 million dollars* per year in JavaScript error logging storage costs. Wow, talk about a good deal!

There are a lot of amazing things that are made possible by ember-concurrency, but unfortunately I cannot describe them all in this post. Please check out the wonderful documentation site, which features in-depth explanations, fancy animations, and useful examples to get you started. You can also read Alex Matchneer’s post about it or watch his EmberConf talk.

(*Actual results may vary.)

How ember-concurrency improved things

Charlie using the `.enqueue()` task modifier.

In early 2017, we brought in Mike North to give the team a few days of Ember training. We’d recently committed to rewriting our main application from Backbone to Ember and were looking to train ourselves up, so that we had the tools and tricks to succeed. (Spoiler: it worked.) One of the most important power-ups Mike introduced us to was ember-concurrency.

We went over some of our scheduling application’s code and Mike showed us a few examples of how we could refactor our Promise nests using ember-concurrency. The idea was to manage only what we need to manage, and leverage as much derived state as possible to reduce the potential for mistakes. The whole room cheered. We hoisted Mike up onto our shoulders and went to get pizza.

When we got back to working on our scheduling application, we were able to refactor away a lot of the manual state tracking we were doing and use the wonderful derived state from ember-concurrency instead. We went from being professional Promise Wranglers to becoming Task Tango-ers, which sounds far more delightful. Here’s what our appointment form code looks like on Tasks:

This refactoring was cool. We’re certainly managing less state ourselves, but it doesn’t really show off how powerful ember-concurrency can be. To get deeper into ember-concurrency, we need to look at a more difficult problem, one that’s more hairy to solve without ember-concurrency.

Building for resiliency and recovery

Let’s say you’re a small, but growing healthcare delivery company. You’ve got your area of focus, and you’re really good at it, but you need to provide support for something very complicated — prescriptions management — that cannot be done with the resources you currently have. (Maybe you’ll get there one day. I believe in you!) You would likely engage a vendor that provides the service you’re after. However, unless you’re a really good negotiator or hold the vendor’s mascot hostage, vendors work with lots of companies, not just your own, and cannot guarantee 100% reliability. Their systems are out of your control and sometimes they experience issues. Sometimes they experience issues more frequently than you’d like.

No one can predict these things but it’s important to be prepared by building in resiliency and recovery opportunities into the UI. Topics like fault tolerance are usually discussed at the systems and network level, but there’s a place for them at the UI level too.

At Iora, our colleagues load our application and get a big hunk of JavaScript over the wire. Then, their browser parses and executes the JavaScript. There’s a non-trivial cost to loading that all up again, so we don’t want to force them to reload the page if we don’t need to. If a user clicks a button which makes a call to a degraded or unavailable service and the application crashes, that’s no fun. The user might even think it was their fault. And that’s terrible.

We’re working hard to bring bearded Robin Williams technology to our platform.

Handling the unhappy path

Imagine you’re using Promises, and you need to provide a level of resiliency or fault tolerance to an operation.

  1. You need to be able to tell the user when something went wrong.
  2. The user should also be given the option to retry the operation.
  3. Later, you’d like to be able to retry the operation automatically without any interaction from the user. You’d like to target the automatic retries on errors consistent with transient failures the vendor is known to experience.

How would you do it?

A lot of people would look at such a problem and come to the very reasonable conclusion that it’s not worth the effort. Others might tackle it with similar but domain-tailored solutions, which would then need to be maintained.

An error occurred, we’re sorry

Let’s look at the first requirement related to telling the user what went wrong. This is easy with ember-concurrency, and in fact there are a few ways to do it: you could check the isError property on the latest task instance in your template to show a message or you could listen for the errored lifecycle event on the task and trigger a flash message. Here’s how we use the isError property and the derived error state to show when there’s a problem with our prescriptions service:

This is a quick win with ember-concurrency. We don’t have to manually track if the request failed ourselves or why it failed. It’s all attached to the task instance, and we don’t have to think about resetting any state.

Enabling recovery

Now, what about letting the user retry a request? This is real easy with ember-concurrency too. Here’s how we added a retry button:

Admittedly, you could do this with a regular action as well. But without ember-concurrency, you’d need to manually manage that error state and probably incorporate some sort of state reset within the retry button click. Yuck.

Extending ember-concurrency to enable automatic retries

ember-concurrency provides a really powerful level of primitives, which help reduce a lot of manual state tracking and boilerplate for common asynchronous UI patterns. However, ember-concurrency is not immune to boilerplate when it comes to more complex patterns.

Take for example, a long-running, looping background task, in which we want to try and recover from a certain number of errors before we give up. We can solve a lot of this with the ember-concurrency primitives we have, but end up with a lot of manual state tracking for things like “how many times has this task failed?” In our application, we have a set of tasks like this for maintaining our user’s session while continuously using the application. Here’s how we did this using only regular ember-concurrency:

Excerpts from a session service.

Taking a look at the keepAlivePollingTask, there is a lot going on in there. Most of the complexity is surrounding tracking state for how many requests have failed, how many have run, and adapting what happens next based on that.

The above approach does result in Working Software™. It tracks that state, shows different notifications to the user when we’re trying to reconnect them, and alerts them when they might get automatically logged-out due to inactivity. However, it is a very rigid implementation. The retry and failure tracking logic is as-is. It’s easy enough to reduce or increase the amount of failures allowed, or change the interval between checks. If we wanted to do something more complicated, like implementing exponential backoff, we’d need to revamp the state tracking and restructure some things, rather than swapping something out or changing a setting.

ember-concurrency-retryable

That’s where ember-concurrency-retryable comes in. It’s an Ember add-on that hooks into ember-concurrency to provide support for automatic retrying of tasks. (Disclosure: I am the author of ember-concurrency-retryable, and I alone am responsible for its faults.)

Implemented as a new task modifier (like .drop() or .restartable()), you can enable retryable behavior for any tasks and delegate control to reusable policies for how the task will be retried. Below is a simple example of how it can be added to tasks:

Please see the usage docs for more information.

If at first you don’t succeed, retry, retry, retry, retry again

Refactoring the session service using ember-concurrency-retryable, we’re able to get rid of a lot of the inner complexity in the keepAlivePolling task. In addition, we’re able to leverage the events interface of ember-concurrency and ember-concurrency-retryable to react to the task, rather than complicate our task function. Because ember-concurrency-retryable is controlling the retry behavior, we can tweak and swap out the retry policy with something that fits with some future requirements, without retooling the session service code.

Below is the refactored session service. As can be seen on lines 26–46, the keepAlivePolling task is much less complex now that we’re not tracking as much state ourselves. Instead, the logout and disconnected handlers are called in response to events according to the lifecycle

A refactored session service.

Our session service still has some oddities related to legacy behavior we need to iron out. However, it’s a much more understandable bit of code, now that we’ve been able to leverage more advanced ember-concurrency features like events, waitForProperty, and race, and use ember-concurrency-retryable to refactor away the complexity around retrying.

Conclusion

As more application complexity moves to the frontend, having a robust way to manage asynchronous application behavior and the concurrency problems inherent in the browser is imperative in order to provide a consistent and reliable user experience. Promises and async functions are wonderful primitives, but they’re just that: primitives. A toolkit like ember-concurrency and power-ups like ember-concurrency-retryable provide the control you need to design interactions you can reason about, so you can stay focused on the problems you need to solve for your colleagues, users, customers, and, in our case, patients. If you’re still out there in the Async Sea, drowning in Promises, I strongly encourage you to join us aboard the USS Ember-Concurrency. We’re shipping out full-steam ahead! (I couldn’t think of anymore bird puns.)

Intensive Code Unit

Iora Health Engineering

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store