Analizar las limitaciones de una aplicación web basada en sondeo

Completado

Aplicación web basada en sondeo.

La arquitectura actual de la aplicación notifica información sobre acciones mediante la recuperación de toda la información sobre el precio de las acciones desde el servidor en función de un temporizador. Este diseño se suele denominar diseño basado en sondeo.

Server

La información sobre el precio de las acciones se almacena en una base de datos de Azure Cosmos DB. Cuando se desencadena mediante una solicitud HTTP, la función getStocks devuelve todas las filas de la base de datos.

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,
        };
    },
});
  • Obtener datos: La primera sección de código, cosmosInput, obtiene todos los elementos de la tabla stocks, con la consulta SELECT * from c, en la base de datos stocksdb de Cosmos DB.
  • Devolver datos: La segunda sección del código, app.http, recibe esos datos en la función como entrada en context.extraInputs y, a continuación, los devuelve como el cuerpo de la respuesta al cliente.

Remoto

El cliente de ejemplo usa Vue.js para componer la interfaz de usuario y el cliente Fetch para controlar las solicitudes a la API.

La página HTML usa un temporizador para enviar una solicitud al servidor cada cinco segundos para solicitar existencias. La respuesta devuelve una matriz de acciones, que, luego, se muestran al usuario.

<!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();
    }
});

Una vez que el método startPoll comienza el sondeo, se llama al método update cada cinco segundos. Dentro del método update, se envía una solicitud GET al punto de conexión de la API de /api/getStocks y el resultado se establece en app.stocks, lo que actualiza la interfaz de usuario.

El código de cliente y servidor es relativamente sencillo: obtener todos los datos, mostrar todos los datos. Como encontramos en nuestro análisis, esta simplicidad aporta algunas limitaciones.

Análisis de la solución prototipo

Como ingeniero de Tailwind Traders, ha identificado algunas de las desventajas de este enfoque de sondeo basado en temporizador.

  • Solicitudes de API innecesarias: En el prototipo de sondeo basado en temporizador, la aplicación cliente se pone en contacto con el servidor tanto si hay cambios en los datos subyacentes como si no.

  • Actualizaciones de página innecesarias: Una vez que se devuelven datos desde el servidor, la lista completa de existencias se actualiza en la página web, incluso si no ha cambiado ningún dato. Este mecanismo de sondeo es una solución ineficaz.

  • Intervalos de sondeo: La selección del mejor intervalo de sondeo para el escenario también supone un reto. El sondeo obliga a elegir entre el costo de cada llamada al back-end y la rapidez con la que se quiere que la aplicación responda a los nuevos datos. Los retrasos a menudo existen entre el momento en que los nuevos datos están disponibles y el momento en que la aplicación los detecta. En la ilustración siguiente se muestra el problema.

    Ilustración en la que se muestra una escala de tiempo y un desencadenador de sondeo que comprueban los nuevos datos cada 5 minutos. Los nuevos datos estarán disponibles después de 7 minutos. La aplicación no es consciente de los nuevos datos hasta el siguiente sondeo, que se produce a los 10 minutos.

    En el peor de los casos, el retraso potencial para detectar datos nuevos es igual que el intervalo de sondeo. Así que, ¿por qué no usar un intervalo menor?

  • Cantidad de datos: A medida que la aplicación crece, la cantidad de datos intercambiada entre el cliente y el servidor se convierte en un problema. Cada encabezado de solicitud HTTP incluye cientos de bytes de datos junto con la cookie de la sesión. Toda esta sobrecarga, especialmente si hay una carga elevada, crea recursos desperdiciados y afecta innecesariamente al servidor.

Ahora que está más familiarizado con el prototipo, es el momento de ejecutar la aplicación en la máquina.

Compatibilidad de CORS

En el archivo local.settings.json de la aplicación de Functions, la sección Host incluye la siguiente configuración.

{
  "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
  }
}

Esta configuración permite que una aplicación web que se ejecuta en localhost:3000 realice solicitudes a la aplicación de función que se ejecuta en localhost:7071. La propiedad CORSCredentials indica a la aplicación de función que acepte las cookies de credencial de la solicitud.

El uso compartido de recursos entre orígenes (CORS) es una característica de HTTP que permite que una aplicación web que se ejecuta en un dominio acceda a recursos de otro dominio. Los exploradores web implementan una restricción de seguridad denominada directiva del mismo origen que impide que una página web llame a las API de otro dominio diferente; CORS proporciona una forma segura de permitir que un dominio (el dominio de origen) llame a las API de otro dominio.

Cuando se ejecuta localmente, CORS se configura automáticamente en el archivo local.settings.json del ejemplo, que nunca se publica. Al implementar la aplicación cliente (unidad 7), también debe actualizar la configuración de CORS en la aplicación de funciones para permitir el acceso desde la aplicación cliente.