Fixing a bug involved making new media, mailing them out, and somehow getting customers to install the new release. Even now, when updates can be easily and automatically downloaded, it can be a struggle to get customers to run the latest and greatest. Engineering would rather be working on an exciting new feature than trying to debug an old version. Our customers use our software because they want to make their lives easier; they aren’t interested in our internal versioning scheme. They want to tell their customer they’ve patched that hole in the wall; they don’t want to download a patch and restart. Through the magic of the SaaS (software as a service) model, we can make sure everyone is always running our very best software.
The user-facing part of Gridium’s Tikkit software is an Ember application. It lives in the cloud; specifically, in an AWS S3 bucket. When a user types their Tikkit URL into the browser, they download the application as a package of minified, gzipped JavaScript code. When they click on links within the application, they load just the new data that they need; they don’t re-download the whole application. The application responds quickly, because it’s already loaded, and just fetching a small amount of fresh data (usually less than 10k). Yay modern software architecture!
But wait! An Ember app is a single-page application. Once a user has loaded the application, they don’t need to load it again. They can leave their browser open for days or weeks, and it will continue doing its job: request data, display data. So then what happens when a Gridium engineer fixes a bug or adds a new feature? How do we make sure the user installs our latest and greatest? The short answer is we tell their browser to do it for them.
We use CircleCI to build our deployment package. After running an ember build
, we save the Circle build number to a build.txt
file, zip everything up, and save it as a build artifact. A deploy fetches the package from Circle and puts in an S3 bucket. (Read all about it in my Deploying Ember Apps to S3 blog post.) The build.txt
has Cache-Control
set to no-cache
, so that users will always fetch a fresh copy. Here’s the relevant section of our circle.yml
file:
deployment: prod: branch: /.*/ commands: - node_modules/ember-cli/bin/ember build --environment=production - echo $CIRCLE_BUILD_NUM > dist/build.txt - tar czvf $CIRCLE_ARTIFACTS/dist.tar.gz dist
Each deployed build includes a build.txt
to identify it. Next, we need to tell the end user’s browser to do something with that information. I wrote an Ember mixin to do this.
– checkVersion
method compares the current time agains a versionCheckDue
property in local storage.
– if it’s time to check the version, it loads build.txt
(always the latest and greatest, since Cache-control
says no-cache
).
– it sets the next version check to 1 day in the future
– it checks the user’s current version, stashed in local storage
– if there isn’t a current version, it sets the just-loaded version as current
– if the user has a different version, they reload to get a fresh one.
Here’s the code:
checkVersion: function() { var due = localStorage.getItem('versionCheckDue'); var now = moment(); // X = Unix timestamp, in seconds (1360013296) if (due && moment(due, 'X').isAfter(now)) { // not expired yet return; } // get deployed version this.get('api').ajaxWithoutAuth({url: '/build.txt'}).then((response) => { let availableBuild = response.trim(); let build = localStorage.getItem('build'); now.add(1, 'days'); localStorage.setItem('versionCheckDue', now.format('X')); if (!build) { // first load localStorage.setItem('build', availableBuild); return; } if (build !== availableBuild) { localStorage.removeItem('build'); // get fresh version of the app! location.reload(); } }); }
This mixin is defined in an addon that we use in several related projects. To actually do the version checks, we include the addon, then run the check version method in the beforeModel
on the application route:
import VersionCheck from 'huyang-common/mixins/version-check'; export default Ember.Route.extend(VersionCheck, ApplicationRouteMixin, { beforeModel: function(transition) { this._super(transition); this.checkVersion(); },
On every route change within the app, we check the version. Usually, this is only super-fast comparison: is it time to ask what’s new? If it is, we asynchronously fetch the latest version number. If there’s something new, the browser requests a whole new app, along with the data to display. This runs in beforeModel
because at that point:
- the browser has the new target URL (for example, /request/123)
- the user is allowed to see this URL (
_super
redirects if not) - the browser has not yet requested any new data (specified in the
model
hook)
A reload makes the browser load the whole application again, along with any supporting data. It’s exactly the same as if the user had pasted the URL directly into their browser. As a result of clicking on something in the app, the user now has the latest version of the application, and can continue working without interruption.
We don’t tell the user they’re doing it wrong. We don’t interrupt their workflow and make them go do something else that they’re not even sure they want. We figure out if we’ve got something new and improved, then serve it up. Who wants day-old software? Fresh baked is so much better!