Analyze the limitations of a polling-based web app

Completed

Polling-based web application.

The application's current architecture reports stock information by fetching all stock price information from the server based on a timer. This design is often called a polling-based design.

Server

The stock price information is stored in an Azure Cosmos DB database. When triggered by an HTTP request, the function getStocks returns all rows from the database.

import { app, input } from "@azure/functions";

const cosmosInput = input.cosmosDB({
    databaseName: 'stocksdb',
    containerName: 'stocks',
    connection: 'COSMOSDB_CONNECTION_STRING',
    sqlQuery: 'SELECT * from c',
});

app.http('getStocks', {
    methods: ['GET'],
    authLevel: 'anonymous',
    extraInputs: [cosmosInput],
    handler: (request, context) => {
        const stocks = context.extraInputs.get(cosmosInput);
        
        return {
            jsonBody: stocks,
        };
    },
});
  • Get data: The first section of code, cosmosInput, gets all the items in the stocks table, with the query SELECT * from c, in the stocksdb database in Cosmos DB.
  • Return data: The second section of code, app.http, receives that data into the function as an input in context.extraInputs then returns it as the response body back to the client.

Client

The sample client uses Vue.js to compose the UI and the Fetch client to handle requests to the API.

The HTML page uses a timer to send a request to the server every five seconds to request stocks. The response returns an array of stocks, which are then displayed to the user.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css" integrity="sha256-8B1OaG0zT7uYA572S2xOxWACq9NXYPQ+U5kHPV1bJN4=" crossorigin="anonymous" />
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">
    <link rel="stylesheet" href="style.css">
    <title>Stocks | Enable automatic updates in a web application using Azure Functions and SignalR</title>
</head>
<body>
    
    <!-- BEGIN: Replace markup in this section -->
    <div id="app" class="container">
        <h1 class="title">Stocks</h1>
        <div id="stocks">
            <div v-for="stock in stocks" class="stock">
                <div class="lead">{{ stock.symbol }}: ${{ stock.price }}</div>
                <div class="change">Change:
                    <span :class="{ 'is-up': stock.changeDirection === '+', 'is-down': stock.changeDirection === '-' }">
                        {{ stock.changeDirection }}{{ stock.change }}
                    </span></div>
            </div>
        </div>
    </div>
    <!-- END  -->

    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js" integrity="sha256-chlNFSVx3TdcQ2Xlw7SvnbLAavAQLO0Y/LBiWX04viY=" crossorigin="anonymous"></script>
    <script src="bundle.js" type="text/javascript"></script>
</body>
</html>
import './style.css';

function getApiUrl() {

    const backend = process.env.BACKEND_URL;
    
    const url = (backend) ? `${backend}` : ``;
    return url;
}

const app = new Vue({
    el: '#app',
    interval: null,
    data() { 
        return {
            stocks: []
        }
    },
    methods: {
        async update() {
            try {
                
                const url = `${getApiUrl()}/api/getStocks`;
                console.log('Fetching stocks from ', url);

                const response = await fetch(url);
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }

                app.stocks = await response.json();
            } catch (ex) {
                console.error(ex);
            }
        },
        startPoll() {
            this.interval = setInterval(this.update, 5000);
        }
    },
    created() {
        this.update();
        this.startPoll();
    }
});

Once the startPoll method begins polling, the update method is called every five seconds. Inside the update method, a GET request is sent to the /api/getStocks API endpoint and the result is set to app.stocks, which updates the UI.

The server and client code is relatively straightforward: get all data, display all data. As we find out in our analysis, this simplicity brings with it some limitations.

Analysis of prototype solution

As a Tailwind Traders engineer, you've identified some of the drawbacks of this timer-based polling approach.

  • Unnecessary API requests: In the timer-based polling prototype, the client application contacts the server whether or not changes exist to the underlying data.

  • Unnecessary page refreshes: Once data is returned from the server, the entire list of stocks is updated on the web page, even if no data has changed. This polling mechanism is an inefficient solution.

  • Polling intervals: Selecting the best polling interval for your scenario is also a challenge. Polling forces you to make a choice between how much each call to the backend costs and how quickly you want your app to respond to new data. Delays often exist between the time that new data becomes available and the time that the app detects it. The following illustration shows the issue.

    An illustration showing a timeline and a polling trigger checking for new data every five minutes. New data becomes available after seven minutes. The app isn't aware of the new data until the next poll, which occurs at 10 minutes.

    In the worst case, the potential delay for detecting new data is equal to the polling interval. So why not use a smaller interval?

  • Amount of data: As the application scales, the amount of data exchanged between the client and server becomes a problem. Each HTTP request header includes hundreds of bytes of data along with the session's cookie. All this overhead, especially when under heavy load, creates wasted resources and unnecessarily taxes the server.

Now that you're more familiar with the prototype, it's time to get the application running on your machine.

Supporting CORS

In the local.settings.json file of the Functions App, the Host section includes the following settings.

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "<STORAGE_CONNECTION_STRING>",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
    "COSMOSDB_CONNECTION_STRING": "<COSMOSDB_CONNECTION_STRING>"
  },
  "Host" : {
    "LocalHttpPort": 7071,
    "CORS": "http://localhost:3000",
    "CORSCredentials": true
  }
}

This configuration allows a web application running at localhost:3000 to make requests to the function app running at localhost:7071. The property CORSCredentials tells the function app to accept credential cookies from the request.

Cross-origin resource sharing (CORS) is an HTTP feature that enables a web application running under one domain to access resources in another domain. Web browsers implement a security restriction known as same-origin policy that prevents a web page from calling APIs in a different domain; CORS provides a secure way to allow one domain (the origin domain) to call APIs in another domain.

When running locally, CORS is configured for you in the sample's local.settings.json file, which is never published. When you deploy the client app (unit 7), you have to also update the CORS settings in the function app to allow access from the client app.