Skip to content

Writing Openmix Applications

Jacob Wan edited this page Jul 21, 2015 · 12 revisions

Writing Openmix Applications

This document describes the recommended method of developing custom Openmix applications.

It is infeasible to reproduce the exact operating environment of Openmix on a typical user's workstation so we validate Openmix applications using a unit-test framework. Unit testing allows the developer to exercise the application code on their local workstation. Otherwise, it would be necessary to upload the application to Cedexis' servers after every slight change to see if it works. This would be time-consuming and frustrating, because it's not always easy to tell if code is behaving properly. With test-driven development, the developer can be assured that the code works as intended.

This documentation is intended to help developers understand how to leverage this technique, with specific information on writing tests for applications using the Openmix API. Readers already familiar with test-driven development may be able to skim large parts of this document, but certain sections should be useful to anyone, such as Anatomy of an Openmix Application, Writing Unit Tests, and Openmix API Reference.

We encourage you to look through the code and tests for the applications in the library to see several examples of writing Openmix applications and unit tests to exercise them. The two files app.js and tests/test.js are the files to pay the most attention to.

Setup

Openmix applications are written in JavaScript and use node.js to execute. node.js needs to be installed in order to run tests. refer to Setting Up for more information on how to install node.js in your environment.

After node is installed, open the terminal to the openmixapplib/apps/<app_name>/ directory and install app dependencies with the following command:

$ npm install

Example Unit Tests

Each application in the library comes with a suite of unit tests written to test the particular application. You can run these tests by opening the terminal to the openmixapplib/apps-javascript/<app_name>/ directory and running the command:

$ ./run-tests.sh

or you can run the tests directly with:

$ ./node_modules/karma/bin/karma start ./karma.app.conf.js

A Mini Tutorial

New Project Template

To make it easy to begin working on a new Openmix application, a basic template is provided at openmixapplib/apps-javascript/template/. To start a new application, simply copy the template directory to a location of your choice. Once copied, rename the new directory from "template" to something else, such as "my-openmix-project". We'll assume this is the name of your application's project directory through the remainder of this documentation.

Here are some of the important files and directories provided by the template:

  • app.js - This file contains your Openmix application code. You will extend this class to add functionality to your application. To deploy your application, you will upload this file to our servers via the Cedexis customer portal.

  • run-tests.sh - This file provides an easy way to run the unit tests.

  • validate-js.sh - This file provides an easy way to run the JSHint validation scripts.

  • tests/test.js - This file contains a basic test suite that you can extend to exercise the code in your application class.

Execute Your Project's Test Suite

A basic test suite is provided in tests/tests.js. The version provided with the template does little more than execute the default methods that come with your application. These methods are mostly empty by default, so there's really nothing to test. However, it shows how you'll go about running the test suite as you go forward.

At a command prompt, navigate into the my-openmix-project directory and execute:

$ run-tests.sh

You should see output similar to the following::

Running Openmix application unit tests

INFO [karma]: Karma v0.10.10 server started at http://localhost:9876/
INFO [launcher]: Starting browser PhantomJS
INFO [PhantomJS 1.9.8 (Mac OS X)]: Connected on socket AehRs7KhEER8HBaaS0Ex
PhantomJS 1.9.8 (Mac OS X): Executed 2 of 2 SUCCESS (0.06 secs / 0.006 secs)

All unit tests passed

This shows that the basic plumbing is okay, i.e. the unit test runner is executed, the application class is recognized and its default test methods are run successfully.

Anatomy of an Openmix Application

Use your favorite text editor whenever directed to open a file below.

Now you're ready to extend the template project to create a real Openmix application. Start by opening my-openmix-project/app.js. Notice that the file defines two methods, init and onRequest. Your OpenmixApplication class provides implementations of these two methods to create its functionality. It may be useful to look at some of the examples found in the SDK to see how it's done.

The init method

This method is used to declare the runtime expectations of the application. This includes:

  • The inputs that the application expects to use (e.g. Radar HTTP RTT data, Pulse Live data, various Ankeena stats, etc.)
  • The reason codes that the application expects to use. A reason code is like a "tag" that can be assigned to a request, useful in reporting.
  • The various response options that the application may produce (CNAMEs).

The application object's init method is executed exactly once, immediately following instantiation.

The onRequest method

This method contains the application's per-request business logic. It is called whenever the application receives a request, and is expected to select a response option.

In the event that no provider is selected, Openmix will deliver the "fallback" CNAME that is configured when you upload or update the application in the Cedexis portal. For more information on fallbacks, see Writing Openmix Applications - Fallbacks.

To ensure the most effective results and proper reporting, every execution path within the application object's onRequest method should result in the selection of a provider. You might wish to designate a default provider explicitly in the code.

Reason Codes

A good way to facilitate useful reporting is to use reason codes. Reason codes explain why the application selected a result CNAME. To use reason codes, assign them when making a decision in the onRequest function using the setReasonCode method on the Response object .

Here's an example:

function OpenmixApplication(settings) {
    'use strict';

    var reasons = {
        got_expected_market: 'A',
        geo_override_on_country: 'B',
        unexpected_market: 'C'
    };

    this.handle_request = function(request, response) {
        if (settings.country_to_provider !== undefined
            && settings.country_to_provider[request.country] !== undefined) {
            // Override based on the request country
            decision_provider = settings.country_to_provider[request.country];
            decision_ttl = decision_ttl || settings.default_ttl;
            decision_reason = all_reasons.geo_override_on_country;
        }
        else {
            decision_provider = settings.default_provider;
            decision_ttl = decision_ttl || settings.error_ttl;
            decision_reason = all_reasons.unexpected_market;
        }

        response.respond(decision_provider, settings.providers[decision_provider].cname);
        response.setTTL(decision_ttl);
        response.setReasonCode(decision_reason);
    };

Writing Unit Tests

Take a look at the simple unit test suite provided in my_openmix_project/tests/tests.js. It contains two functions, test_do_init and test_handle_request. These methods contain the tests for the init and onRequest functions, respectively. The actual tests are contained within each 'test' function call within the those two methods.

To create a unit test, add a new test function call with a callback function that executes the test setup and verification. In the verification function, call the assert functions to verify the expected values are produced. When the Karma test runner executes a test, if any expectations are not met, it raises an exception.

To create a new test, add a new test function call:

    test('new test', test_do_init({
        setup: function(i) {
            // Setup code here
        },
        verify: function(i) {
            // Assertion code here
        }
    }));

To create a new test, there are usually three parts to a test: 1. the settings object, 2. the request setup and 3. the test verification.

For example, take the following example:

    test('geo country overrides', test_handle_request({
        //
        // Populate a Settings object with test values
        //
        settings: {
            providers: {
                'foo': {
                    cname: 'www.foo.com'
                },
                'bar': {
                    cname: 'www.bar.com'
                },
                'baz': {
                    cname: 'www.baz.com'
                }
            },
            country_to_provider: { 'UK': 'bar' },
            market_to_provider: { 'EG': 'foo' },
            default_provider: 'foo'
        },
        //
        // Setup the test request
        //
        setup: function(i) {
            console.log(i);
            i.request.country = 'UK';
            i.request.market = 'EG';
        },
        //
        // Verify the expected results
        //
        verify: function(i) {
            console.log(i);
            equal(i.response.respond.callCount, 1, 'Verifying respond call count');
            equal(i.response.setTTL.callCount, 1, 'Verifying setTTL call count');
            equal(i.response.setReasonCode.callCount, 1, 'Verifying setReasonCode call count');

            equal(i.response.respond.args[0][0], 'bar', 'Verifying selected alias');
            equal(i.response.respond.args[0][1], 'www.bar.com', 'Verifying CNAME');
            equal(i.response.setTTL.args[0][0], 20, 'Verifying TTL');
            equal(i.response.setReasonCode.args[0][0], 'B', 'Verifying reason code');
        }
    }));

In Openmix applications, the settings object is often the method by which configuration settings are provided to the application script. In this example, the application is expecting information about the available providers and which providers are configured for a specified geographic area.

    test('geo country overrides', test_handle_request({
        //
        // Populate a Settings object with test values
        //
        settings: {
            providers: {
                'foo': {
                    cname: 'www.foo.com'
                },
                'bar': {
                    cname: 'www.bar.com'
                },
                'baz': {
                    cname: 'www.baz.com'
                }
            },
            country_to_provider: { 'UK': 'bar' },
            market_to_provider: { 'EG': 'foo' },
            default_provider: 'foo'
        },

        ...

    }));

The setup function allows the unit test to set the values that will be returned by the request object. If this function is empty, the request object will be setup with default values. This function can be used to test requests that come from specific locations or that have specific values expected in the test.

    test('geo country overrides', test_handle_request({
        
        ...

        //
        // Setup the test request
        //
        setup: function(i) {
            console.log(i);
            i.request.country = 'UK';
            i.request.market = 'EG';
        },
        
        ...

    }));

The verify function is where the script results are tested. The application decision is available from the response object that is available from the function argument.

    test('geo country overrides', test_handle_request({
        
        ...

        //
        // Verify the expected results
        //
        verify: function(i) {
            console.log(i);
            equal(i.response.respond.callCount, 1, 'Verifying respond call count');
            equal(i.response.setTTL.callCount, 1, 'Verifying setTTL call count');
            equal(i.response.setReasonCode.callCount, 1, 'Verifying setReasonCode call count');

            equal(i.response.respond.args[0][0], 'bar', 'Verifying selected alias');
            equal(i.response.respond.args[0][1], 'www.bar.com', 'Verifying CNAME');
            equal(i.response.setTTL.args[0][0], 20, 'Verifying TTL');
            equal(i.response.setReasonCode.args[0][0], 'B', 'Verifying reason code');
        }
    }));

Application Guidelines

Avoid unnecessary looping

Even if performance isn't the main goal of your Openmix application, you'll still want to be sure that it performs as well as possible. One way is to avoid unnecessary looping. Instead, try using lookup objects that require less processing.

Do as much as possible in the initialization

Any calculations that generate results that are static across requests, such as mapping providers to geos, should be moved to the init function. The OnRequest function is run on every request so it should be as efficient as possible.

Application Hostname Prefix

Openmix subdomains can be called with an additional hostname prefix which is made available to the Openmix application. The application can then use the hostname prefix to alter it's decision logic.

For example, if the Openmix subdomain for an application is '2-01-29a4-0001.cdx.cedexis.net', a hostname can be prefixed: 'hostname.2-01-29a4-0001.cdx.cedexis.net'. The prefix value of 'hostname' will be available to the application via the request.hostname_prefix property.

Here are the basic steps to use a hostname prefix in an Openmix app:

  1. Upload the app using the Portal.

  2. Obtain the app address from the Portal. This is the portion of the app ID to the right of "*.". For example, if the Portal says the app ID is *.2-01-29a4-0001.cdx.cedexis.net, then its address is 2-01-29a4-0001.cdx.cedexis.net.

  3. Create CNAME records, directing requests for one or more of your subdomains to the app address. Optionally, you can supply an additional marker to the beginning of the app address. This is called "hostname", for historical reasons. For example, you could create the following CNAME records::

     www         IN  CNAME  www.2-01-29a4-0001.cdx.cedexis.net
     video       IN  CNAME  video.2-01-29a4-0001.cdx.cedexis.net
     downloads   IN  CNAME  downloads.2-01-29a4-0001.cdx.cedexis.net
    

    You can then use this hostname marker in the application logic:

var handler = new OpenmixApplication({
    servers: {
        'www': {
            'www1': 'www1.example.com',
            'www2': 'www2.example.com',
            'www3': 'www3.example.com'
        },
        'video': {
            'video1': 'video1.example.com',
            'video2': 'video2.example.com',
            'video3': 'video3.example.com'
        },
        'downloads': {
            'downloads1': 'downloads1.example.com',
            'downloads2': 'downloads2.example.com',
            'downloads3': 'downloads3.example.com'
        }
    },
    ...
});

...

    this.handle_request = function(request, response) {
        var hostname = request.hostname_prefix;
        var candidates = settings.servers[hostname];

        // Select from the server candidates
    };

Clone this wiki locally