Wednesday, May 1, 2013

CDNs fail, but your scripts don't have to - fallback from CDN to local jQuery

CDNs fail, but your scripts don't have to - fallback from CDN to local jQuery:
CDN issues in the Northeast
There's a great website called http://whoownsmyavailability.com that serves as a reminder to me (and all of us) that external dependencies are, in fact, external. As such, they are calculated risks with tradeoffs. CDNs are great, but for those minutes or hours that they go down a year, they can be super annoying.
I saw a tweet today declaring that the ASP.NET Content Delivery Network was down. I don't work for the CDN team but I care about this stuff (too much, according to my last performance review) so I turned twitter to figure this out and help diagnose it. The CDN didn't look down from my vantage point.
I searched for things like "ajax cdn,"microsoft cdn," and "asp.net cdn down" and looked at the locations reported by the Twitter users in their profiles. They had locations like CT, VT, DE, NY, ME. These are all abbreviations for states in the northeast of the US. There were also a few tweets from Toronto and Montreal. Then, there was one random tweet from a guy in Los Angeles on the other side of the country. LA doesn't match the pattern that was developing.
I tweeted LA guy and asked him if he was really in LA or rather on the east coast.
@shanselman @attiladelisle Oh weird. It's back for me because I'm in CA and I unplugged my persistent VPN to MA :)
— Alex Whittemore (@alexwhittemore) April 30, 2013
Bingo. He was VPN'ed into Massachusetts (MA). I had a few folks send me tracerts and sent them off to the CDN team who fixed the issue in a few minutes. There was apparently a bad machine in Boston/NYC area that had a configuration change specific to the a certain Ajax path that had gone undetected by their dashboard (this has been fixed and only affected the Ajax part of the CDN in this local area).
More importantly, how can we as application developers fallback gracefully when an external dependency like a CDN goes down? Just last week I moved all of my Hanselminutes Podcast images over to a CDN. If there was a major issue I could fall back to local images with a code change. However, if this was a mission critical site, I should not only have a simple configuration switch to fallback to local resources, but I should also test and simulate a CDN going down so I'm prepared when it inevitably happens.
With JavaScript we can detect when our CDN-hosted JavaScript resources like jQuery or jQuery UI aren't loaded successfully and try again to load them from local locations.

Falling back from CDN to local copies of jQuery and JavaScript

The basic idea for CDN fallback is to check for a type or variable that should be present after a script load, and if it's not there, try getting that script locally. Note the important escape characters within the document.write. Here's jQuery:
<script src="http://ajax.aspnetcdn.com/ajax/jquery/jquery-2.0.0.min.js"></script>

<script>

if (typeof jQuery == 'undefined') {

    document.write(unescape("%3Cscript src='http://www.hanselman.com/js/jquery-2.0.0.min.js' type='text/javascript'%3E%3C/script%3E"));

}

</script>
Or, slightly differently. This example uses protocol-less URLS, checks a different way and escapes the document.write differently.
<script src="http://www.hanselman.com//ajax.aspnetcdn.com/ajax/jquery/jquery-2.0.0.min.js"></script>

<script>window.jQuery || document.write('<script src="js/jquery-2.0.0.min.js">\x3C/script>')</script>
If you are loading other plugins you'll want to check for other things like the presence of specific functions added by your 3rd party library, as in "if (type of $.foo)" for jQuery plugins.
Some folks use a JavaScript loader like yepnope. In this example you check for jQuery as the complete (loading) event fires:
yepnope([{

  load: 'http://ajax.aspnetcdn.com/ajax/jquery/jquery-2.0.0.min.js',

  complete: function () {

    if (!window.jQuery) {

      yepnope('js/jquery-2.0.0.min.js');

    }

  }

}]);
Even better, RequireJS has a really cool shorthand for fallback URLs which makes me smile:
requirejs.config({

    enforceDefine: true,

    paths: {

        jquery: [

            '//ajax.aspnetcdn.com/ajax/jquery/jquery-2.0.0.min',

            //If the CDN location fails, load from this location

            'js/jquery-2.0.0.min'

        ]

    }

});



//Later

require(['jquery'], function ($) {

});
With RequireJS you can then setup dependencies between modules as well and it will take care of the details. Also check out this video on Using Require.JS in an ASP.NET MVC application with Jonathan Creamer.

Updated ASP.NET Web Forms 4.5 falls back from CDN automatically

For ASP.NET Web Forms developers, I'll bet you didn't know this little gem. Here's another good reason to move your ASP.NET sites to ASP.NET 4.5 - using a CDN and falling back to local files is built into the framework.
Fire up Visual Studio 2012 and make a new ASP.NET 4.5 Web Forms application.
When using a ScriptManager control in Web Forms, you can set EnableCdn="true" and ASP.NET will automatically change the <script> tags from using local scripts to using CDN-served scripts with local fallback checks included. Therefore, this ASP.NET WebForms ScriptManager:
<asp:ScriptManager runat="server" EnableCdn="true">

    <Scripts>

        <asp:ScriptReference Name="jquery" />

        <asp:ScriptReference Name="jquery.ui.combined" />

    </Scripts>

</asp:ScriptManager>
...will output script tags that automatically use the CDN and automatically includes local fallback.
<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.8.2.js" type="text/javascript"></script>

<script type="text/javascript">

//<![CDATA[

(window.jQuery)||document.write('<script type="text/javascript" src="Scripts/jquery-1.8.2.js"><\/script>');//]]>

</script>



<script src="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.24/jquery-ui.js" type="text/javascript"></script>

<script type="text/javascript">

//<![CDATA[

(!!window.jQuery.ui && !!window.jQuery.ui.version)||document.write('<script type="text/javascript" src="Scripts/jquery-ui-1.8.24.js"><\/script>');//]]>

</script>
What? You want to use your own CDN? or Googles? Sure, just make a ScriptResourceMapping and put in whatever you want. You can make new ones, replace old ones, put in your success expression (what you check to make sure it worked), as well as your debug path and minified path.
var mapping = ScriptManager.ScriptResourceMapping;

// Map jquery definition to the Google CDN

mapping.AddDefinition("jquery", new ScriptResourceDefinition

{

    Path = "~/Scripts/jquery-2.0.0.min.js",

    DebugPath = "~/Scripts/jquery-2.0.0.js",

    CdnPath = "http://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js",

    CdnDebugPath = "https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.js",

    CdnSupportsSecureConnection = true,

    LoadSuccessExpression = "window.jQuery"

});



// Map jquery ui definition to the Google CDN

mapping.AddDefinition("jquery.ui.combined", new ScriptResourceDefinition

{

    Path = "~/Scripts/jquery-ui-1.10.2.min.js",

    DebugPath = "~/Scripts/jquery-ui-1.10.2.js",

    CdnPath = "http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.2/jquery-ui.min.js",

    CdnDebugPath = "http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.2/jquery-ui.js",

    CdnSupportsSecureConnection = true,

    LoadSuccessExpression = "window.jQuery && window.jQuery.ui && window.jQuery.ui.version === '1.10.2'"

});
I just do this mapping once, and now any ScriptManager control application-wide gets the update and outputs the correct fallback.
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.js" type="text/javascript"></script>

<script type="text/javascript">

//<![CDATA[

(window.jQuery)||document.write('<script type="text/javascript" src="Scripts/jquery-2.0.0.js"><\/script>');//]]>

</script>



<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.2/jquery-ui.js" type="text/javascript"></script>

<script type="text/javascript">

//<![CDATA[

(window.jQuery && window.jQuery.ui && window.jQuery.ui.version === '1.10.2')||document.write('<script type="text/javascript" src="Scripts/jquery-ui-1.10.2.js"><\/script>');//]]>

</script>
If you want to use jQuery 2.0.0 or a newer version than what came with ASP.NET 4.5, you'll want to update your NuGet packages for ScriptManager. These include the config info about the CDN locations. To update (or check your current version against the current) within Visual Studio go to Tools | Library Package Manager | Manage Libraries for Solution, and click on Updates on the left there.
image
Regardless of how you do it, remember when you setup Pingdom or other availability alerts that you should be testing your Content Delivery Network as well, from multiple locations. In this case, the CDN failure was extremely localized and relatively short but it could have been worse. A fallback technique like this would have allowed sites (like mine) to easily weather the storm.


© 2013 Scott Hanselman. All rights reserved.


DIGITAL JUICE

No comments:

Post a Comment

Thank's!