Etiquetado: Azure

Migrando el tres en raya: Azure SDK 2.4, SignalR 2.1.1, Windows 8.1

El año pasado escribí una serie de tres artículos donde explicaba cómo crear un juego de tres en raya para jugar online en tiempo real, desde el navegador y también desde una app Windows 8. Como la tecnología avanza muy rápido, al cabo de un mes de escribir el artículo el código ya no compilaba con los últimos SDK. Así que, ahora que estoy de vacaciones me he dedicado a procrastinar y a actualizar alguno de mis proyectos personales para no tener que hacer lo que me había propuesto :). Aquí os dejo los pasos a realizar para migrar, aunque podéis descargar directamente el nuevo paquete aquí.

Como primer paso, conviene tener Visual Studio 2013.3 instalado y actualizar el SDK de Windows Azure al último disponible (2.4 por ahora).
Sigue leyendo

Anuncios

Indice secuencial con Table Storage en Azure

Azure Table Storage es un sistema NoSQL de almacenamiento de tipo clave/valor. Es un sistema pensado para grandes volúmenes de datos distribuidos en alta disponibilidad y algunas cosas que son triviales hoy en día en un motor SQL estándar, en un NoSQL es muy posible que ni siquiera estén contempladas. Esto nos obliga a pensar en otras técnicas o incluso otras formas de almacenar los datos y sus relaciones.
Numbers

Hace unos días necesité un contador secuencial para almacenar en tablas del Table Storage. Ya se que muchos me diréis que no es lo más adecuado para el Storage, pero la secuencia es para mostrar por pantalla y resulta que los humanos llevamos muy mal el retener números de más de seis cifras.
Sigue leyendo

Juegos sociales online con SignalR y Windows Azure (3 de 3)

Eight!
En los artículos anteriores de esta serie hemos visto cómo utilizar SignalR para proporcionar un canal de comunicaciones desde el servidor hacia el cliente. Además, modificamos el código para funcionar en Azure para mejorar la escalabilidad de nuestro servicio. Llegamos al final de la serie con un reto que me he puesto a mi mismo, utilizar lo que hemos hecho desde una aplicación de Windows 8 y que no se nos complique el artículo.

SignalR tiene librerías cliente tanto para .Net como JavaScript. Como os prometí un artículo sencillo, utilizaremos JavaScript, porque así aprovecharemos el código que ya tenemos de forma casi directa dentro de nuestra aplicación.

App de tres en raya

Como sabéis, Windows 8 te permite utilizar librerías estándar como jQuery, lo más probable es que lo que hicimos en la versión web nos funcione con muy pocos cambios. Así que… ¡Manos a la obra!
Empezamos creando una aplicación de la tienda Windows en HTML5/JavaScript:

3er.30.w8project

Para conectarnos a nuestro Hub necesitaremos las librerías cliente de SignalR. De nuevo Nuget nos ayudará a instalarlas:

3er.30.w8jsclient

HTML

El código HTML será casi el mismo que el que hicimos en nuestro default.html, sólo cambiaremos una cosa, pues no podemos utilizar la función prompt para pedir el nombre de usuario; mientras no tengamos un login, ponemos una caja de texto en pantalla. Recordad que estoy enfocando los ejemplos al uso básico de SignalR, conectarnos al sistema de autentificación ya lo haremos en otro momento:

<body>
    <label for="userName">Nombre del jugador: </label><input type="text" id="userName" />
    <div id="partida">        
...

Cliente del Hub

SignalR nos permite definir manualmente las llamadas al Hub directamente, pero también genera automáticamente un proxy con el código para nosotros en http://127.0.0.1/signalr/hubs. Si abrimos esta dirección con el Internet Explorer intentará descargarse el archivo JavaScript autogenerado:
descargarhub

Nos guardarmos el archivo en la carpeta js de nuestro proyecto para poder referenciarlo desde la página default.html. Cuando abramos el script veremos que en la línea donde se define el signalR.hub se utiliza un path relativo. Nos bastará cambiar el path por uno absoluto que apunte a nuestro servidor de Azure. Como por ahora seguimos usando el emulador, apuntamos a la dirección de bucle local 127.0.0.1:

signalR.hub = $.hubConnection("http://127.0.0.1/signalr", { useDefaultPath: false });

El script tresenraya.js también lo copiamos a la carpeta js y modificamos la llamada por la lectura en el campo username que hemos creado antes:

//prompt("Escribe tu nombre");
$username.change(null, function (e) {
    nombre = e.target.value;
});

Definimos la variable $username junto a las otras:

$username = $("#userName");

Y ya casi lo tenemos, sólo nos queda enlazar a los scripts y cambiar el tema a light para que se vea parecido a nuestra página. La etiqueta head de la página default.html de nuestra app queda como esta:

<head>
    <meta charset="utf-8" />
    <title>TresEnRapp</title>

    <!-- WinJS references -->
    <link href="//Microsoft.WinJS.1.0/css/ui-light.css" rel="stylesheet" />
    <script src="//Microsoft.WinJS.1.0/js/base.js"></script>
    <script src="//Microsoft.WinJS.1.0/js/ui.js"></script>

    <!-- TresEnRapp references -->
    <link href="/css/default.css" rel="stylesheet" />

    <script src="Scripts/jquery-1.6.4.min.js"></script>
    <script src="Scripts/jquery.signalR-1.1.2.min.js"></script>
    <!--Reference the autogenerated SignalR hub script. -->
    <script src="/js/hub.js"></script>
    <script src="js/tresenraya.js"></script>
    <script src="/js/default.js"></script>

</head>

Y aquí tenéis la aplicación en Windows 8 jugando contra otra en IE10:

3er.24.sidebysidew8

Forzar el uso de WebSockets

Nuestra aplicación Windows 8 ha funcionado bien, pero si miramos la salida de la consola de JavaScript veremos los siguientes mensajes:
3er.25.websockets
Tenemos dos mensajes de error. El primero proviene de una comprobación que hace jQuery para ajustarse a las capacidades de cada navegador. El segundo debe preocuparnos un poco más, nuestra aplicación no está utilizando WebSockets y podría hacerlo.
El mensaje nos lo dice claro, no hemos habilitado las llamadas cross-domain en SignalR, aunque ha sido lo suficientemente listo como para encontrar otra manera de funcionar sin WebSockets. Antes no necesitábamos habilitar las llamadas cross-domain pues el código se ejecutaba en la página proveniente del sitio, pero ahora hemos creado el front-end dentro de una app de la tienda Windows, así que la llamada es cross-domain. Para habilitarlo sólo tenemos que configurar SignalR antes de arrancarlo en el Global.asax.cs:

var hubConfiguration = new HubConfiguration();
hubConfiguration.EnableCrossDomain = true;
// Register the default hubs route: ~/signalr
RouteTable.Routes.MapHubs(hubConfiguration);

Al ejecutar de nuevo la aplicación ya habrá desaparecido el error y estaremos aprovechando todo el potencial que nos proporciona Windows 8.

¿Y ahora qué?

Tal como os prometí, hacer que nuestra app funcionara no ha sido complicado. Crear una aplicación válida para la tienda Windows y que además tenga éxito ya es otra historia.
Ahora es vuestro turno. Nos quedan, entre otras cosas, las siguientes tareas:

  • Autentificación: podemos utilizar los sistemas estándar de autentificación de usuarios y luego aplicar el atributo Authorize para permitir o denegar el acceso a métodos y servicios.
  • Control de conexión y desconexión de usuarios: SignalR nos avisa cuando ocurre una conexión/desconexión/reconexión de usuario, nosotros “sólo” tendremos que gestionar esos cambios de estado para saber si un usuario está online o no.
  • Windows 8: esta aplicación es sólo una prueba de concepto. Hacer que nuestra aplicación brille nos costará un poco más, para conseguirlo tenemos grandes consejos en el MSDN

Espero que os haya gustado. Podéis descargar el código completo del ejemplo en el siguiente enlace de Codeplex.

Juegos sociales online con SignalR y Windows Azure (2 de 3)

Clouds
En el artículo anterior vimos cómo utilizar SignalR para crear servicios con un canal de comunicaciones abierto con el cliente. Recordad que es siempre el cliente el que establece la comunicación y luego el servidor utiliza la técnica más adecuada en cada caso para mantener el canal abierto.

El ejemplo, aunque con bastante código, no deja de ser un caso básico y con poca escalabilidad. Si queremos dar servicio a miles o millones de usuarios simultáneos, necesitaremos que nuestro servicio pueda crecer a lo ancho y no a lo alto. En lugar de aumentar potencia de CPU y RAM a una sola máquina, nos permite ir añadiendo más máquinas a medida que las necesitemos, que trabajarán en paralelo y nos permitirán un número de usuarios simultáneos sin límite.
Con Windows Azure podemos hacer esto y mucho más. Para que nuestra aplicación funcione en un entorno Cloud realizaremos algunos cambios en la misma:

  • Hasta ahora estábamos almacenando las partidas en memoria, en una aplicación en la nube no podemos utilizar esta técnica porque trabajaremos con múltiples instancias y cada una tiene su propia memoria. Necesitamos un lugar de almacenamiento que puedan compartir las diferentes instancias. En Azure tenemos diferentes posibilidades: Windows Azure SQL para datos relacionales, Windows Azure Table Storage para datos no relacionales (NoSQL), o Windows Azure Caching si necesitamos algo pequeño y muy rápido. En nuestro ejemplo usaremos las tablas de Windows Azure Storage.
  • Añadiremos un enlace al ServiceBus, de manera que cada vez que uno de los servidores necesite enviar información a todos los clientes conectados al servicio pueda avisar al resto de servidores del cluster para que también envíen esa información a sus clientes.

Qué necesito

Os recuerdo que para este ejercicio necesitamos:

Convertir un proyecto web en proyecto Azure

El primer paso es añadir a nuestra solución un proyecto de Azure que nos configurará el paquete de despliegue en la nube. Pulsamos el botón derecho sobre el proyecto y nos aparecerá la opción Add Windows Azure Cloud Service Project:

3er.11.AddCloudService

Si ejecutamos nuestra aplicación ahora, el Visual Studio arrancará el emulador de Windows Azure y nuestra aplicación se ejecutará en el entorno simulado. Al principio nos parecerá que funciona todo, para comprobar que en realidad nos va a fallar todo nos basta con configurar el rol para que se ejecuten dos instancias.

En el proyecto TresEnRaya.Azure, abrimos la carpeta Roles y hacemos doble-click en nuestro rol TresEnRaya, en la configuración podemos cambiar el número de instancias:
3er.11.increasinstances

Al incrementar el número de instancias, haremos que cada nueva conexión vaya a una máquina distinta, es decir, se irán balanceando las conexiones. Como os he comentado antes, las instancias no comparten memoria ni cpu, son instancias completamente independientes, incluso en el simulador. Con nuestro diseño de aplicación con listas en memoria nos encontramos con un problema importante: cada instancia tiene su lista de usuarios, solicitudes y partidas y no se ven entre ellas. Como podemos ver en la siguiente imagen, el botón para jugar contra “Manolo” debería estar en dos de los navegadores y sólo aparece en uno:
3er.11.ymanolo

Windows Azure Storage

Para almacenar las partidas y las solicitudes y que todas las instancias de nuestro servicio tengan acceso vamos a utilizar el Azure Table Service del Windows Azure Storage.
Utilizaremos el Storage en lugar de SQL Azure porque la sencillez de los datos nos lo permite y es un almacenamiento mucho más económico que el SQL.

Una explicación rápida: las tablas de Windows Azure son listas organizadas por clave, pueden contener hasta 252 valores en cada registro y tienen un límite de 100TB. Los puntos clave de las tablas en Azure que vamos a encontrarnos durante el desarrollo de esta aplicación son:

  • Los elementos se identifican mediante una clave compuesta por dos elementos PartitionKey y RowKey.
  • La información está agrupada por PartitionKey, de tal manera que recuperar múltiples registros de una misma PartitionKey es muy rápido.
  • Realizar consultas que impliquen diferentes PartitionKey o buscar por otras propiedades que no formen parte de la clave penaliza el rendimiento
  • No existen las relaciones entre tablas, es decir, no podremos hacer consultas cruzadas, olvidad lo que sabéis de SQL y las reglas de normalización de tablas. Tendremos que trabajar de otra manera, seguramente repitiendo datos organizados de formas distintas en múltiples tablas.

Definición de las tablas

En C# las tablas se pueden definir directamente desde clases, podremos utilizar las mismas clases que ya teníamos, heredando de la clase TableEntity:

public class Jugador:TableEntity
{
    public Jugador()
    {
    }

    public Jugador(string pais, string nombre, string id)
    {
        PartitionKey = pais;
        RowKey = nombre;
        Id = id;
    }

    public string Pais { get { return PartitionKey; } }
    public string Nombre { get { return RowKey; } }
    public string Id { get; set; }
}

Al transformar la clase necesitamos añadir un constructor por defecto y convertimos los campos país y nombre en la PartitionKey y RowKey respectivamente.
Así podremos realizar consultas sobre todos los jugadores de un mismo país sin perder rendimiento.
El problema que nos encontraremos será encontrar el jugador por Id de conexión, algo que hacemos bastante dentro de la clase DatosPartida. Como una consulta por Id nos penalizará el rendimiento, lo que haremos será crear otra tabla para poder buscar por Id.

public class JugadorPorId : TableEntity
{
    public JugadorPorId() { 
    }

    public JugadorPorId(string pais, string nombre, string id)
    {
        PartitionKey = pais;
        RowKey = id;
        Nombre = nombre;
    }

    public string Pais { get { return PartitionKey; } }
    public string Nombre { get; set; }
    public string Id { get { return RowKey; } }
}

El caso de las partidas es más complicado, pues los jugadores vienen de otra tabla y no podemos guardar en las tablas árboles de objetos, tienen que ser objetos bastante planos. En nuestro caso vamos a modificar un poco la forma en que se guardan esas propiedades en los métodos WriteEntity y ReadEntity

public class Partida:TableEntity
{
    public const int Dimension = 3;
    public Partida()
    {
    }

    public Partida(string pais, string id)
    {
        PartitionKey = pais;
        RowKey = id;
        _tablero = new char[Dimension * Dimension];
        for (int i = 0; i < _tablero.Length; i++)
        {
            _tablero[i] = ' ';
        }
    }

    public string Pais { get { return PartitionKey; } }
    public string Id { get { return RowKey; } }
    public Jugador Jugador1 { get; set; }
    public Jugador Jugador2 { get; set; }
    public int Turno { get; set; }

    public override IDictionary<string, EntityProperty> WriteEntity(OperationContext operationContext)
    {
        var context= base.WriteEntity(operationContext);
        context.Remove("Jugador1");
        context.Add("Jugador1Nombre", EntityProperty.GeneratePropertyForString(Jugador1.Nombre));
        context.Add("Jugador1Id", EntityProperty.GeneratePropertyForString(Jugador1.Id));
        context.Remove("Jugador2");
        context.Add("Jugador2Nombre", EntityProperty.GeneratePropertyForString( Jugador2.Nombre));
        context.Add("Jugador2Id", EntityProperty.GeneratePropertyForString(Jugador2.Id));
        return context;
    }
    public override void ReadEntity(IDictionary<string, EntityProperty> properties, 
        OperationContext operationContext)
    {
        Jugador1 = new Jugador {
            PartitionKey= properties["Pais"].StringValue,
            RowKey = properties["Jugador1Nombre"].StringValue ,
            Id = properties["Jugador1Id"].StringValue
        };
        Jugador2 = new Jugador {
            PartitionKey= properties["Pais"].StringValue,
            RowKey = properties["Jugador2Nombre"].StringValue ,
            Id = properties["Jugador2Id"].StringValue
        };
        base.ReadEntity(properties, operationContext);
    }

(no pongo el resto de código pues es igual al código del capítulo anterior).

Creación de las tablas en Azure

Hemos creado las entidades por código y las tablas las vamos a crear igual. Modificaremos la clase DatosPartida que ya teníamos definida, para que al arrancar cree las tablas si es que no existen.

public class DatosPartida
{
    static CloudTable _solicitudes;
    static CloudTable _partidas;
    static CloudTable _jugadores;
    static CloudTable _jugadoresPorId;

    static DatosPartida()
    {
        // Retrieve the storage account from the connection string.
        CloudStorageAccount storageAccount = CloudStorageAccount.Parse(
            CloudConfigurationManager.GetSetting("StorageConnectionString"));

        // Create the table client.
        CloudTableClient tableClient = storageAccount.CreateCloudTableClient();

        // Create the table if it doesn't exist.
        _solicitudes = tableClient.GetTableReference("Solicitudes");
        _solicitudes.CreateIfNotExists();

        _partidas = tableClient.GetTableReference("Partidas");
        _partidas.CreateIfNotExists();

        _jugadores = tableClient.GetTableReference("Jugadores");
        _jugadores.CreateIfNotExists();

        _jugadoresPorId = tableClient.GetTableReference("JugadoresPorId");
        _jugadoresPorId.CreateIfNotExists();
    }

En el constructor estático estamos conectando al servicio de tablas mediante una cadena de conexión que he puesto en la configuración del rol. Para definirla hacemos doble click sobre el rol:
3er.12.webrole
Y en la sección Settings podremos definir nuestro valor de configuración. Por ahora utilizaremos el emulador local del storage.
3er.13.storage
A partir de este punto, modificamos los métodos de acceso que utilizábamos antes sobre listas para que accedan a las tablas. Como veréis he procurado no consultar las tablas sin una PartitionKey:

     public DatosPartida()
     {
     }

     public Jugador NuevoJugador(string pais, string nombre, string id)
     {
         var jugador = new Jugador(pais, nombre, id);
         TableOperation insertJugador = TableOperation.InsertOrReplace(jugador);
         _jugadores.Execute(insertJugador);

         TableOperation insertJugadorId = TableOperation.InsertOrReplace(new JugadorPorId(id, pais, nombre));
         _jugadoresPorId.Execute(insertJugadorId);
         return jugador;
     }

     public Jugador NuevaSolicitud(Jugador jugador)
     {
         var solicitud = new Jugador(jugador.Pais, jugador.Nombre, jugador.Id);
         TableOperation operation = TableOperation.InsertOrReplace(solicitud);
         _solicitudes.Execute(operation);
         return solicitud;
     }

     public Jugador ObtenerSolicitud(string pais, string nombre)
     {
         var op = TableOperation.Retrieve<Jugador>(pais, nombre);
         var result = _solicitudes.Execute(op);
         var solicitud = result.Result as Jugador;
         return solicitud;
     }

     public bool BorrarSolicitud(Jugador solicitud)
     {
         var op = TableOperation.Delete(solicitud);
         var result = _solicitudes.Execute(op);
         return result.HttpStatusCode == 204;
     }

     public Partida EmpezarPartida(string pais, Jugador jugador1, Jugador jugador2)
     {
         var partida = new Partida(pais, Guid.NewGuid().ToString())
         {
             Jugador1 = jugador1,
             Jugador2 = jugador2
         };
         var empiezaPartidaOp = TableOperation.InsertOrReplace(partida);
         _partidas.Execute(empiezaPartidaOp);
         return partida;
     }

     public Jugador ObtenerJugador(string pais, string id)
     {
         var op = TableOperation.Retrieve<JugadorPorId>(pais, id);
         var jugadorxid = _jugadoresPorId.Execute(op).Result as JugadorPorId;
         if (jugadorxid != null)
         {
             op = TableOperation.Retrieve<Jugador>(pais, jugadorxid.Nombre);
             return _jugadores.Execute(op).Result as Jugador;
         }
         return null;
     }

     public Partida ObtenerPartida(string pais, string id)
     {
         var op = TableOperation.Retrieve<Partida>(pais, id);
         return _partidas.Execute(op).Result as Partida;
     }

     public void GuardarMovimiento(Partida partida)
     {
         var replaceOp = TableOperation.Replace(partida);
         _partidas.Execute(replaceOp);
     }

     public IEnumerable<Jugador> ListaDisponibles(string pais)
     {
         var solicitudQuery = new TableQuery<Jugador>().Where(
            TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, pais));
         return _solicitudes.ExecuteQuery<Jugador>(solicitudQuery);
     }
 }

ServiceBus y SignalR

Si ejecutamos ahora la aplicación, parece que funciona™ pero tiene un gran fallo que podremos comprobar aumentando el número de instancias. Como vimos al principio del post, las diferentes instancias no se hablan entre sí, lo que provoca que si tenemos dos clientes de nuestra aplicación y cada uno está conectado a una instancia diferente, nuestra aplicación no funcionará, o sólo lo hará a medias.
Windows Azure tiene un mecanismo para resolver esto, el Service Bus, que nos permite crear suscripciones a la información agrupadas por “temas”. De esta manera, cuando algo cambie en una instancia podemos avisar a todas las otras.
Por suerte, SignalR implementa esta funcionalidad con el ServiceBus, así que sólo tendremos que crear una cuenta de ServiceBus y conectarla a nuestra aplicación, SignalR se encargará de gestionar los canales.

El ServiceBus no tiene emulador. En la versión anterior podíamos instalarlo en local, pero todavía no han publicado la nueva, así que para poder utilizarlo, incluso en local, tendremos que crear uno en una cuenta de Azure. El coste del ServiceBus es muy pequeño (en la fecha de publicación del artículo €0,0075 al mes por cada 10.000 mensajes) así que no nos vamos a arruinar por hacer unas pruebas.
Si tenéis alguna cuenta MSDN os entrará dentro de los recursos gratuitos que tenéis. Si no es así, podéis crear una cuenta de evaluación gratuita durante un mes: http://www.windowsazure.com/es-es/pricing/free-trial/

En nuestro portal de Azure creamos un espacio de nombres para nuestra aplicación, yo he sido muy original y lo he llamado tresenraya:
3er.20.servbus
Recordad ponerlo en una región que esté cerca de vuestros usuarios, pues es muy conveniente que todos los servicios que vamos a usar estén en la misma región, evita tráfico innecesario.
Una vez esté activo abrimos la información de la conexión y la copiamos:

3er.23.signalrsbconect
Esta información de conexión la guardaremos otra vez en las propiedades del rol:

3er.21.servbusconn

Para poder utilizar el ServiceBus necesitaremos importar con nuget el paquete de Microsoft.AspNet.SignalR.ServiceBus:
3er.22.signalrsb

Una vez instalada la librería para el ServiceBus, sólo nos queda avisar a SignalR que debe utilizarlo, así en el Application_Start de Global.asax.cs indicaremos a SignalR qué debe hacer:

protected void Application_Start(object sender, EventArgs e)
{
    // Register the default hubs route: ~/signalr
    RouteTable.Routes.MapHubs();

    var sbConnectionString = CloudConfigurationManager.GetSetting("ServiceBusConnectionString");
    GlobalHost.DependencyResolver.UseServiceBus(sbConnectionString, "TresEnRaya");
}

Resumen

En este capítulo hemos modificado la aplicación de juego para que funcionara bien dentro de un entorno cloud. El mayor trabajo ha sido cambiar el sistema de almacenamiento, pues el Hub de SignalR no lo hemos tocado y sólo hemos tenido que añadir dos líneas de código para que SignalR funcione correctamente con múltiples instancias.
En el próximo artículo crearemos la aplicación cliente en Windows 8. Como ya hemos hecho lo difícil, os prometo que la app de Windows 8 será coser y cantar.

Descarga el código del ejemplo de Codeplex

Juegos sociales online con SignalR y Windows Azure (1 de 3)

EOD technicians play tic-tac-toe with a children at a family day picnic

Hoy os propongo una mini-serie de 3 capítulos sobre SignalR y Windows Azure con un ejemplo de juego sencillo: el tres en raya, en el que podremos jugar contra otro oponente online.

La tarea presenta algunas complicaciones que resolveremos gracias a SignalR y Azure de forma muy sencilla:

  • Notificaciones: al realizar cada jugada debemos notificar al oponente. Hay muchas maneras de recibir esta notificación, dependiendo del navegador que utilicemos podemos hacer polling, que consiste en ir pidiendo novedades al servidor de forma cíclica, long polling o utilizar websockets para recibir la jugada. SignalR nos simplificará la tarea detectando automáticamente las capacidades de nuestro navegador y utilizando la técnica más adecuada en cada uno.
  • Escalabilidad: ¿qué pasa si nuestro juego tiene éxito y tenemos millones de usuarios simultáneos? Necesitaremos que el servicio escale y para eso tenemos Windows Azure, utilizaremos un Web Role para el servicio y Azure Storage Tables para almacenar la información.
  • Comunicaciones: al escalar a lo ancho nos encontraremos con otro problema, los clientes de un servidor no reciben información de los otros servidores, deberemos establecer un canal de comunicaciones entre los servidores para que reciban qué están haciendo los demás. Para ello utilizaremos el Service Bus de Azure, que nos proporciona un modelo de eventos y suscripciones http://www.asp.net/signalr/overview/performance-and-scaling/scaleout-in-signalr

Atacaremos estos puntos clave uno por uno y en este primer capítulo habrá bastante código. Montaremos todo el sistema base para el juego. Hoy empezamos creando la aplicación básica con SignalR, guardando la información en listas en memoria como en los ejemplos básicos, en el siguiente capítulo le añadiremos Azure y en el último añadiremos otro cliente al sistema, además del cliente web que hacemos hoy.

Antes de empezar os dejo un par de “disclaimers”:

Nota para puristas: voy a hacer bastantes simplificaciones para que este artículo sea didáctico, aunque me duela, no voy a utilizar MVVM, MVC, TDD ni nada que me aparte de lo que pretendo mostrar hoy, el uso de SignalR en Azure.

Nota para pragmáticos: no os esperéis poder hacer copy/paste del código para vuestra aplicación en producción. Aparte de lo comentado en la advertencia anterior, el ejemplo no será completo, voy a dejar algunas cosas como ejercicios para que hagáis vosotros 🙂

Y ahora que ya no tengo ninguna responsabilidad puedo empezar a hackear tranquilo.

¿Qué necesito?

Para poder compilar y probar el código vas a necesitar:

SignalR

Para crear cualquier juego multijugador online en tiempo real tenemos que establecer canales de comunicación entre todas las partes implicadas. Normalmente no es algo trivial, más si añadimos diferentes plataformas de cliente, tales como web, aplicaciones móviles y de tableta. Para que funcione bien tendremos que gestionar los posibles problemas de conexión, establecer puntos de conexión entre clientes diferentes, crear un canal de difusión para enviar notificaciones a todos los clientes, confirmar que los mensajes llegan, crear librerías de cliente que mantengan la conexión abierta con el servidor, autorizar e identificar a los usuarios y un largo etcétera de funcionalidades.

Cuando se trata de un cliente Web, añadimos otra vuelta de tuerca, pues debemos tener en cuenta las capacidades de cada navegador para decidir en el momento qué tecnología de comunicaciones funcionará mejor.

SignalR es una librería para ASP.NET que nos liberará de gestionar todos estos problemas y nos permitirá concentrarnos en la tarea que realmente queremos hacer. Nos proporciona una librería en servidor para gestionar todas las conexiones y suscripciones, además de generar dinámicamente un script que nos permitirá realizar la conexión desde el cliente.

Un tres en raya social

Partimos de una aplicación sencilla como el tres en raya para no complicar demasiado el ejemplo, algo intermedio entre  el ejemplo básico de chat y el juego online ShootR.

Empezaremos con una aplicación ASP.NET vacía:

tictactoe_01_newproject

Una vez tengamos nuestra solución, necesitaremos un archivo Global.asax y una página HTML donde mostrar nuestro tablero:

3er.01a.Globalasax

En el mismo menú seleccionamos HTML Page y la llamamos default:

3er.01b.defaulthtmlDentro del body de la página colocamos un panel de tres en raya:

<div id="partida">
    <button id="empezar">Empezar nueva partida</button>
    <style>
        #tablero {
            height: 160px;
            width: 160px;
            padding: 5px;
        }

        .row {
            height: 50px;
            width: 150px;
        }

        .col {
            height: 48px;
            width: 48px;
            border: 1px solid black;
            float: left;
            text-align: center;
            line-height: 48px;
        }

        .row0, .row2 {
            background-color: rgba(0,0,0,0.2);
        }

        .col1 {
            background-color: rgba(0,0,0,0.2);
        }
    </style>

    <div id="tablero">
        <div class="row row0">
            <div class="col col0"></div>
            <div class="col col1"></div>
            <div class="col col2"></div>
        </div>
        <div class="row row1">
            <div class="col col0"></div>
            <div class="col col1"></div>
            <div class="col col2"></div>
        </div>
        <div class="row row2">
            <div class="col col0"></div>
            <div class="col col1"></div>
            <div class="col col2"></div>
        </div>
    </div>

    <div>
        <h2>Solicitud de partidas</h2>
        <div id="partidas">
            <div>Cargando...</div>
        </div>
    </div>
</div>
<div>
    <h2>Mensajes</h2>
    <div id="mensajes"></div>
</div>

En cuanto al código, empezaremos con la parte de servidor. Vamos a utilizar SignalR, necesitaremos añadir las referencias a las librerías en nuestro proyecto. Por suerte tenemos la herramienta Nuget que nos permitirá descargar e instalar en nuestro proyecto todo lo necesario con un click:
3er.02.alt.Nuget
Buscamos en la sección Online por SignalR:
3er.02.signalr
Nuget nos instalará todas las dependencias que necesita la librería y acto seguido nosotros cambiaremos el evento Application_Start en el Global.asax.cs, tal como nos indica el propio SignalR al instalarse:

protected void Application_Start(object sender, EventArgs e)
{
    // Register the default hubs route: ~/signalr
    RouteTable.Routes.MapHubs();
}

Esta pequeña línea realiza la magia que nos permitirá comunicarnos con los clientes desde el servidor. En nuestro proyecto ya podemos crear un Hub, el punto de conexión entre los clientes y el servidor, al que llamaremos PartidaHub:
3er.03.signalrhub

La plantilla nos creará a mínima expresión de un Hub:

public class PartidaHub : Hub
{
    public void Hello()
    {
        Clients.All.hello();
    }
}

Este código de servidor que se ha generado automáticamente, está llamando a una función definida en todos los clientes conectados a este Hub llamada “hello”; SignalR se encargará de que eso ocurra.
En el Hub vamos a crear métodos que podrán ser llamados por el cliente y el mismo hub, a su vez, podrá realizar llamadas a todos los clientes, al emisor del mensaje o a receptores concretos, mediante los siguientes métodos:

//como el anuncio aquel de refrescos...
//para todos
Clients.All.hello();
//para el que llama
Clients.Caller.hello();
//para todos los demás
Clients.Others.hello();
//para uno en concreto
Clients.Client(id).hello();

Si vamos a mirar la definición de estas propiedades y métodos veremos que son de tipo dynamic, lo que nos permite escribir nombres de funciones que no tenemos definidas en ningún sitio de nuestro código C#.

Datos de partida

Antes de ponernos con el Hub creamos unas cuantas estructuras que necesitamos para jugar: los jugadores, las partidas y las solicitudes de partida, para que un jugador pueda empezar una partida y así otro pueda apuntarse.
Creamos una carpeta Data y empezamos a crear unas cuantas clases, la primera una clase para almacenar la partida y la lógica de juego:

public class Partida
{
    public const int Dimension = 3;

    public Partida(string pais, string id)
    {
        Pais = pais;
        Id = id;
        _tablero = new char[Dimension * Dimension];
        for (int i = 0; i < _tablero.Length; i++)
        {
            _tablero[i] = ' ';
        }
    }

    public string Pais { get; set; }
    public string Id { get; set; }
    public Jugador Jugador1 { get; set; }
    public Jugador Jugador2 { get; set; }
    public int Turno { get; set; }

    char[] _tablero;
    public string Tablero
    {
        get
        {
            return new string(_tablero);
        }
        set
        {
            _tablero = value.ToCharArray();
        }
    }

    public bool Marcar(int x, int y, int jugador)
    {
       if (Estado == EstadoPartida.EnJuego)
       {
           var posicion = Dimension * x + y;
           if (jugador == Turno)
           {
               if (_tablero[posicion] == ' ')
               {
                   var valor = Turno == 0 ? 'O' : 'X';
                   _tablero[Dimension * x + y] = valor;
                   Turno = Turno == 0 ? 1 : 0;
                   return ComprobarFinal();
               }
               else
               {
                   throw new InvalidOperationException("La casilla está ocupada");
               }
           }
           else
           {
               throw new InvalidOperationException("No es tu turno");
           }
       }
       else
       {
           throw new InvalidCastException("El juego ha acabado");
       }
   }

   public char Valor(int x, int y)
   {
       return _tablero[Dimension * x + y];
   }

   bool ComprobarFinal()
   {
       int[] jugadas = new int[Partida.Dimension * 2 + 2];

       for (int i = 0; i < Partida.Dimension; i++)
       {
           for (int j = 0; j < Partida.Dimension; j++)
           {
               var v = Valor(i, j);
               int sum = 0;
               if (v == 'X')
               {
                   sum = -1;
               }
               else if (v == 'O')
               {
                   sum = 1;
               }
               jugadas[i] += sum;
               jugadas[Partida.Dimension + j] += sum;
               if (i == j)
               {
                   jugadas[Partida.Dimension * 2] += sum;
               }
               if (i + j == Partida.Dimension - 1)
               {
                   jugadas[Partida.Dimension * 2 + 1] += sum;
               }
           }
       }
       if (jugadas.Count((x) => x == 3) > 0)
       {
           Estado = EstadoPartida.Gana1;
       }
       else if (jugadas.Count((x) => x == -3) > 0)
       {
           Estado = EstadoPartida.Gana2;
       }
       else if (!Tablero.Contains(' '))
       {
           Estado = EstadoPartida.Empate;
       }

       return Estado != EstadoPartida.EnJuego;
   }
}

También necesitaremos una clase Jugador para identificarlos:

public class Jugador
{
    public Jugador(string pais, string nombre, string id)
    {
        Nombre = nombre;
        Pais = pais;
        Id = id;
    }

    public string Pais { get; set; }
    public string Nombre { get; set; }
    public string Id { get; set; }
}

Y una clase DatosPartida que nos permita ir añadiendo jugadores, solicitudes y partidas de forma ordenada. Esta clase nos vendrá bien a la hora de transformar nuestra aplicación a Azure:

public class DatosPartida
{
    List<Jugador> _jugadores = new List<Jugador>();
    List<Partida> _partidas = new List<Partida>();
    List<Jugador> _solicitudes = new List<Jugador>();

    public DatosPartida()
    {
    }

    public Jugador NuevoJugador(string pais, string nombre, string id)
    {
        var jugador=_jugadores.FirstOrDefault(j => j.Pais == pais && j.Nombre == nombre);
        if (jugador == null)
        {

            jugador = new Jugador(pais, nombre, id);
            _jugadores.Add(jugador);
        }
        else
        {
            //actualiza el id del jugador, imaginaremos, por ahora, que si se llaman igual es el mismo...
            jugador.Id = id;
        }
        return jugador;
    }

    public Jugador NuevaSolicitud(Jugador jugador)
    {
        _solicitudes.Add(jugador);
        return jugador;
    }

    public Jugador ObtenerSolicitud(string pais, string nombre)
    {
        return _solicitudes.FirstOrDefault(s => s.Pais == pais && s.Nombre == nombre);
    }

    public bool BorrarSolicitud(Jugador solicitud)
    {
        return _solicitudes.Remove(solicitud);
    }

    public Partida EmpezarPartida(string pais, Jugador jugador1, Jugador jugador2)
    {
        var partida = new Partida(pais, Guid.NewGuid().ToString())
        {
            Jugador1 = jugador1,
            Jugador2 = jugador2
        };
        _partidas.Add(partida);
        return partida;
    }

    public Jugador ObtenerJugador(string pais, string id)
    {
        return _jugadores.FirstOrDefault(jugador => jugador.Id == id);
    }

    public Partida ObtenerPartida(string pais, string id)
    {
        return _partidas.FirstOrDefault(p=>p.Pais==pais && p.Id==id);
    }

    public void GuardarMovimiento(Partida partida)
    {
        var old=ObtenerPartida(partida.Pais, partida.Id);
        _partidas.Remove(old);
        _partidas.Add(partida);
    }

    public IEnumerable<Jugador> ListaDisponibles(string pais)
    {
        return _solicitudes.Where((s)=>s.Pais==pais);
    }
}

Y finalmente escribiremos el Hub, donde cada vez que queramos notificar algo a un cliente o a muchos utilizaremos los métodos dinámicos de Clients.All , Clients.Caller, etc.:

public class PartidaHub : Hub
{
    private readonly static Lazy<DatosPartida> _datos = new Lazy<DatosPartida>(true);
    public DatosPartida Datos { get { return _datos.Value; } }

    public Jugador NuevaPartida(string pais, string nombre)
    {
        var jugador = Datos.NuevoJugador(pais, nombre, Context.ConnectionId);
        var solicitud=Datos.NuevaSolicitud(jugador);
        Clients.All.Nueva(solicitud);  //avisamos a todos que una nueva partida ha sido solicitada
        return jugador;
    }

    public IEnumerable<Jugador> PartidasDisponibles(string pais)
    {
        return Datos.ListaDisponibles(pais);
    }

    public bool Jugar(string pais, string jugador1, string jugador2)
    {
        var solicitud = Datos.ObtenerSolicitud(pais, jugador1);
        if (solicitud != null)
        {
            var jugadorB=Datos.ObtenerJugador(pais, Context.ConnectionId);
            if (jugadorB == null)
                jugadorB = Datos.NuevoJugador(pais, jugador2, Context.ConnectionId);
            Partida partida = Datos.EmpezarPartida(pais, solicitud, jugadorB);

            //eliminamos las solicitudes pendientes de los jugadores
            Datos.BorrarSolicitud(solicitud);
            var solicitud2 = Datos.ObtenerSolicitud(jugadorB.Pais, jugadorB.Nombre);
            if (solicitud2 != null)
            {
                Datos.BorrarSolicitud(solicitud2);
            }

            //notificacmos solicitudes eliminadas
            Clients.All.PartidaEliminada(solicitud);
            if (solicitud2 != null)
            {
                Clients.All.PartidaEliminada(solicitud2);
            }
            //notificamos al llamante sobre el comienzo de la partida
            Clients.Caller.Jugando(partida);
            //notificamos al jugador que solicitó una partida sobre el comienzo de la misma
            Clients.Client(solicitud.Id).Jugando(partida);
            return true;
        }
        else
        {
            error("Ya no existe la partida");
        }
        return false;
    }

    public void MarcaCasilla(string pais, string nombre, int x, int y)
    {
        var partida = Datos.ObtenerPartida(pais, nombre);

        if (partida != null)
        {
            int turno = 0;
            if (Context.ConnectionId == partida.Jugador2.Id)
                turno = 1;
            int turnoAnterior = partida.Turno;

            try
            {
                bool acabada = partida.Marcar(x, y, turno);
                Datos.GuardarMovimiento(partida);
                Clients.Client(partida.Jugador1.Id).Jugada(partida);
                Clients.Client(partida.Jugador2.Id).Jugada(partida);

                if (acabada)
                {
                    switch (partida.Estado)
                    {
                        case EstadoPartida.Empate:
                            Clients.Client(partida.Jugador1.Id).FinJuego("Esta vez hay empate");
                            Clients.Client(partida.Jugador2.Id).FinJuego("Esta vez hay empate");
                            break;
                        case EstadoPartida.Gana1:
                            Clients.Client(partida.Jugador1.Id).FinJuego("Ganaste la partida!!!");
                            Clients.Client(partida.Jugador2.Id).FinJuego("Esta vez te ganó " + partida.Jugador1.Nombre);
                            break;
                        case EstadoPartida.Gana2:
                            Clients.Client(partida.Jugador2.Id).FinJuego("Ganaste la partida!!!");
                            Clients.Client(partida.Jugador1.Id).FinJuego("Esta vez te ganó " + partida.Jugador2.Nombre);
                            break;
                    }
                }
            }
            catch (InvalidOperationException ex)
            {
                mensaje(ex.Message);
            }
        }
        else
        {
            error("no existe la partida");
        }
    }

    private void error(string error)
    {
        mensaje("Error: " + error);
    }

    private void mensaje(string msg)
    {
        Clients.Caller.Mensaje(msg);
    }
}

Código en cliente

En el lado del cliente vamos a definir unas funciones para comunicarnos con el servidor. Lo primero que necesitamos es obtener el Hub que hemos definido antes. SignalR crea para nosotros una librería JavaScript que tiene todo lo necesario para comunicarnos con este.
Creamos en la carpeta Scripts un script “tresenraya.js” y añadimos al final de la página html las referencias a los siguientes scripts, antes del cierre de la etiqueta body:

<script src="Scripts/jquery-2.0.1.min.js"></script>
<script src="Scripts/jquery.signalR-1.1.2.min.js"></script>
<!--Reference the autogenerated SignalR hub script. -->
<script src="/signalr/hubs"></script>
<script src="Scripts/tresenraya.js"></script>

El script /signalr/hubs es el que genera SignalR automáticamente con el código para acceder a nuestros hubs.
Una vez hemos añadido los scripts, podemos empezar a escribir nuestro tresenraya.js. En primer lugar necesitamos conectar al hub e inicializar, en nuestro versión sencilla pediremos el nombre al usuario nada más empezar, lo que vendría a ser el login de los pobres (y confiados). Para el código js usamos jQuery1.6.4 que es el que nos ha instalado SignalR:

$(function () {
    //obtenemos el hub
    var servicioPartidas = $.connection.partidaHub;
    //iniciamos la conexión y una vez iniciada (método done) llamamos a nuestra función de inicialización
    $.connection.hub.start().done(init);

    var pais = "Mallorca";  //para no pedir tantos datos al principio vamos a suponer 
                            //que estáis todos de vacaciones en mi isla ^^

    function init() {
        nombre = prompt("Escribe tu nombre");
        //llamamos a una función de servidor que nos devuelve una lista de partidas
        //que se realizan en Mallorca ^^
        servicioPartidas.server.partidasDisponibles(pais).done(function (list) {
            $partidas.empty();
            $.each(list, function () {
                crearEntrada(this);
            });

        });
        //botón nueva partida
        $btnEmpezar.bind("click", null, function (e) {
            servicioPartidas.server.nuevaPartida(pais, nombre);
            return false;
        });
        //inicializamos los recuadros del 3 en raya
        for (var row = 0; row < 3; row++) {
            for (var col = 0; col < 3; col++) {
                var $celda = $(".row" + row + " .col" + col);
                $celda.click(function () {
                    var x = row;
                    var y = col;
                    return function () {
                        if (partidaActual != null) {
                            servicioPartidas.server.marcaCasilla(partidaActual.Pais, partidaActual.Id, x, y);
                        }
                    }
                }());
            }
        }
    }

Fijaos que estamos llamando a los métodos que definimos en nuestro Hub: nuevaPartida, marcarCasilla, etc., y usamos los mismos parámetros que hemos definido en el código C#.

Para el código anterior necesitamos unas cuantas funciones más que nos permiten mostrar las solicitudes de partida que van llegando:

$btnEmpezar = $("#empezar");
$partidas = $("#partidas");
$mensajes = $("#mensajes");
var partidaActual;
var nombre;
var partidaListItem = "<div id='{Pais}_{Nombre}item'>Pais: {Pais} Usuario:{Nombre} <button id='{Pais}_{Nombre}'>Jugar</button></div>";

// A simple templating method for replacing placeholders enclosed in curly braces.
if (!String.prototype.supplant) {
    String.prototype.supplant = function (o) {
        return this.replace(/{([^{}]*)}/g,
            function (a, b) {
                var r = o[b];
                return typeof r === 'string' || typeof r === 'number' ? r : a;
            }
        );
    };
}

function crearId(entity) {
    return entity.Pais + "_" + entity.Nombre;
}

function crearEntrada(solicitud) {
    if (solicitud.Nombre != nombre) {
        $partidas.append(partidaListItem.supplant(solicitud));
        $("#" + crearId(solicitud)).click(function (e) {
            var valores = e.currentTarget.id.split("_");
            servicioPartidas.server.jugar(valores[0], valores[1],nombre);
            return false;
        });
    }
}

Recibir notificaciones del servidor

Hasta ahora hemos escrito código que envía información, pero también queremos recibir el push que realiza el servidor hacia los clientes. Para esto tenemos que definir unas funciones con los mismos nombres y parámetros que utilizamos en el lado del servidor.
Si recordáis el código anterior, al crear una nueva solicitud de partida llamábamos al método Clients.All.Nueva(solicitud);. Este es el método que tendremos que escribir en el cliente, por ejemplo, con el siguiente código recibimos el mensaje de partidas nuevas:

servicioPartidas.client.nueva = function (solicitud) {
    if (solicitud.Nombre == nombre && solicitud.Pais == pais) {
        //si es tu partida deshabilita el botón de empezar.. ya estás jugando
        $btnEmpezar.text("Esperando a que llegue un jugador");
        $btnEmpezar.prop("disabled", true);
        $partidas.prop("disabled", true);
    }
    else {
        //si no es tu partida saca la lista
        crearEntrada(solicitud);
    }

Aquí cuando se nos notifica el principio y final del juego:

servicioPartidas.client.jugando = function (partida) {
    partidaActual = partida;
    $btnEmpezar.text("Jugando partida " + partida.Jugador1.Nombre + " contra " + partida.Jugador2.Nombre);
    $btnEmpezar.prop("disabled", true);
    $partidas.prop("disabled", true);
    pintarTablero(partida.Tablero);
};

function pintarTablero(tablero) {
    for (var row = 0; row < 3; row++) {
        for (var col = 0; col < 3; col++) {
            var $celda = $(".row" + row + " .col" + col);
            $celda.text(tablero[3 * row + col]);
        }
    }
}

servicioPartidas.client.finJuego = function (mensaje) {
    $mensajes.append(mensaje + "<br/>");
    $btnEmpezar.text("Empezar nuevo juego");
    $btnEmpezar.prop("disabled", false);
    $partidas.prop("disabled", false);
}

Cada vez que hay una jugada desde el servidor se nos manda el tablero y lo volvemos a representar:

servicioPartidas.client.jugada = function (partida) {
    //cuando llega una jugada de nuestra partida en curso
    //pintamos el tablero
    pintarTablero(partida.Tablero);
}

Y dos funciones más que también necesitamos:

//los mensajes también irán apareciendo
servicioPartidas.client.mensaje = function (msg) {
    $mensajes.append(msg + "<br/>");
}

servicioPartidas.client.partidaEliminada = function (solicitud) {
    $("#" + crearId(solicitud) + "item").remove();
}

Tras escribir (o cortapegar) todo este código ya os debería funcionar la primera versión del tres en raya online. Mañana transformaremos el código para que funcione en un WebRole de Windows Azure.
3er.01.pacowins
Espero vuestros comentarios!!! Id pensando cómo haríais que esta aplicación funcione bien en Azure.

Si habéis llegado hasta aquí os merecéis este vídeo viejuno que os explicará por qué he elegido el tres en raya:

Error 401 al actualizar datos en WCF Data Services sobre Azure

401 Unauthorized
Cuando necesitamos acceder a los datos de una manera sencilla desde todas nuestras aplicaciones y posibilitar su uso desde casi cualquier tecnología o dispositivo, una apuesta bastante segura es crear un servicio REST de acceso a los datos.

Para crear un servicio REST de forma muy rápida, Microsoft nos proporciona una herramienta casi mágica llamada WCF Data Services. Esta librería, junto con una base de datos en SQL Azure, nos creará automáticamente un servicio REST en base a un modelo de entidades (Entity Framework, Linq to SQL, etc..), que luego podremos configurar para establecer permisos con unas pocas líneas.

Podéis encontrar multitud de artículos sobre el tema, como por ejemplo este tutorial rápido en MSDN.

Artículo recomendado: Guidance for OData in Windows Azure

Cuando nos funcione el servicio en local, nuestra alma geek nos pedirá probar en Azure incluso antes de configurar la autentificación. Si ese es el caso y la aplicación, además de leer y crear datos, también actualiza y borra, recibiremos un error 401 al realizar estas últimas operaciones.
Esto es así porque las operaciones de lectura y creación se realizan con los verbos GET y POST de HTTP, pero la actualización y borrado de datos se realizan mediante los verbos PUT y DELETE, que en IIS están deshabilitados en el caso de la autentificación anónima.
Sin que sirva de precedente, aquí tenéis un truco rápido para evitar esta situación, pero recordad que debe ser algo temporal y debéis evitar que se puedan guardar, modificar y borrar datos con una conexión anónima!
Es suficiente deshabilitar cualquier tipo de autentificación para evitar el problema. En el web.config de nuestra aplicación añadimos esta entrada en la sección system.web:

<system.web>
   <authentication mode="None" />
</system.web>

Y ya nos debería funcionar, pero recordad habilitar la autentificación de nuevo y establecer mecanismos de seguridad.

Si vuestras necesidades de datos son sencillas y queréis que sea lo más automático y seguro sin esfuerzo, también tenéis los Servicios móviles de Azure

Migración de SQL Server a Windows Azure SQL Database

Ultralight MigrationEsta semana estábamos haciendo una prueba de concepto para una empresa colaboradora y necesitábamos mover una base de datos de unos 2GB a SQL Azure para realizar las pruebas con datos reales. Gracias a Azure y SQL Azure, en unas pocas horas hemos tenido la BBDD lista y funcionando sin problema con la aplicación cliente en una máquina virtual en Azure utilizando vitualización de aplicaciones, pero eso es otra historia que os contará Nacho algún día.

Nuestro trabajo empezó con una entrevista al dba y al desarrollador para que nos contaran cómo utilizaban la BBDD y averiguar si se usaba alguna funcionalidad incompatible con SQL Azure. Hemos encontrado bastantes casos en los que se hacen cross-queries entre diferentes BBDD del mismo servidor e incluso de servidores diferentes, este es uno de los primeros criterios KO que procuramos descartar. Otras funcionalidades incompatibles son los procedimientos almacenados en .NET y algunos campos especiales como filestream.

Suele ocurrir que las BBDD han pasado por muchas manos y mantienen algunos datos y metadatos obsoletos que nadie conoce, así que aunque el cliente nos jure que no está utilizando ninguna de esas funcionalidades, recordad la sabiduría del doctor House: “Everybody lies”.

El asistente de SSMS

En versiones anteriores de las herramientas necesitábamos crear un paquete datpac con las SQL Server Data Tools para después desplegarlo a SQL Azure. Si no queríamos fallar estrepitosamente, era conveniente utilizar alguna herramienta de comprobación de compatibilidad con SQL Azure.
Por suerte, la versión 2012 del Microsoft SQL Server Management Studio (SSMS) viene con un asistente que nos facilitará la migración. Este comprueba casi todas las incompatibilidades antes de empezar el despliegue a Azure y nos ahorrará muchos disgustos.

El asistente de SSMS nos pedirá los datos de conexión al servidor de SQL Azure, el tamaño de la BBDD destino y dónde va a guardar el fichero temporal .bacpac, que contendrá todos los datos necesarios para realizar la migración de la BBDD.

Una vez introducidos los campos que nos pide, el procedimiento comprobará nuestro esquema y nos proporcionará un listado de fallos, para que los vayamos arreglando antes de enviar la carga de datos a Azure:

Vistas

El primer fallo que encontramos nos lo dio en las vistas. Es posible que contengan alguna cross-query y el cliente ya no se acuerde, o que provenga de alguna migración antigua y tenga como prefijo el nombre de la BBDD, por ejemplo:

SELECT CAMPO1, CAMPO2 FROM [BBDD].[USUARIO].[TABLA]

Si se da este caso el procedimiento de migración fallará y tendremos que volver a empezar. Por suerte el wizard de migración nos proporcionará la lista de errores de comprobación y podremos ver qué vistas están fallando para editarlas antes de realizar la migración completa.

Propiedades Extendidas

Las propiedades extendidas son algo que suele pasar desapercibido, probablemente porque sirven para documentar la BBDD. Desde el diseñador podemos poner comentarios en los objetos de la BBDD, estos se guardarán como propiedades extendidas en el esquema de BBDD. Si desde el SSMS pedimos que nos genere el script de una tabla o una vista, muchas veces encontraremos además de la información de esquema, unas llamadas a un procedimiento almacenado llamado sp_addextendedproperty. En algunos casos serán nuestros comentarios, otras veces será información que guarda el propio SSMS cuando diseñamos una tabla o vista de manera visual.

EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'una descripción' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'Table_1', @level2type=N'COLUMN',@level2name=N'ABC'

SQL Azure no permite estas propiedades, así que hay que eliminarlas antes de realizar la migración. En nuestro caso, encontramos muchas propiedades tanto a nivel de campo como a nivel de vista. Borrarlas una por una sería una tarea demasiado larga. Me irrita especialmente tener que repetir más de dos veces cualquier tarea mecánica; por suerte alguien creó un script para encontrar y eliminar todas esas propiedades extendidas. Nosotros lo adaptamos a nuestras necesidades, pues detectamos que sólo teníamos propiedades extendidas para campos y vistas, así que ignoramos las tablas:

select 'EXEC sp_dropextendedproperty
@name = '''+extended_properties.name+'''
,@level0type = ''schema''
,@level0name = ' + object_schema_name(extended_properties.major_id) + '
,@level1type = ''table''
,@level1name = ' + object_name(extended_properties.major_id) + '
,@level2type =  ''column''
,@level2name = ' + columns.name
from sys.extended_properties
join sys.columns
on columns.object_id = extended_properties.major_id
and columns.column_id = extended_properties.minor_id
where extended_properties.class_desc = 'OBJECT_OR_COLUMN'
and extended_properties.minor_id > 0
UNION
select 'EXEC sp_dropextendedproperty
@name = '''+extended_properties.name+'''
,@level0type = ''schema''
,@level0name = ' + object_schema_name(extended_properties.major_id) + '
,@level1type = ''view''
,@level1name = ' + object_name(extended_properties.major_id)
from sys.extended_properties
join sys.views
on views.object_id = extended_properties.major_id
where extended_properties.class_desc = 'OBJECT_OR_COLUMN'

Este código nos devolverá tantas filas como objetos tengamos con propiedades extendidas en campos y vistas. Copiamos el resultado de la consulta y la pegamos en una ventana nueva de consulta. Al ejecutar todos esos comandos que acabamos de crear con la consulta, borraremos todas las propiedades extendidas que nos molestaban.

Tablas, índices y claves

El SQL Azure es bastante tiquismiquis en este sentido y exige que todas las tablas tengan al menos un índice y que los índices sean agrupados (clustered). Siempre te encuentras con algún despistado que creó una tabla sin una PK. A veces ocurre en el caso de tablas de dominio pequeñas, con valores que casi no se usan y no tienen un mantenimiento; puede que estén años así sin que nadie se de cuenta.

Cross-Triggers

Nos aseguramos de que no hacían cross-queries, pero no les preguntamos si hacían cross-triggers y estos, por supuesto, son también incompatibles con SQL Azure. Encontrarlos puede ser un poco más complicado, porque tendríamos que ir uno por uno a ver qué trigger está utilizando esos datos. Por suerte, el sistema de migración crea un archivo llamado BACPAC que contiene un backup completo de la base de datos, con sus procedimientos, triggers, etc, donde podremos encontrarlos.

Si no le hemos dicho lo contrario al SSMS, podremos encontrar el archivo BACPAC dentro de %temp% y aunque la extensión es desconocida (.bacpac) podemos cambiarla a .zip y así ver el contenido del archivo. Encontraremos en él un archivo model.xml con todos los scripts de creación de la BBDD, así que buscando la palabra trigger podremos ir encontrando todos los que hay e investigar si son válidos para Azure o no.

Usuarios

La primera vez que conseguimos desplegar el esquema en SQL Azure nos encontramos usuarios de BBDD pertenecientes a un dominio de Windows, como esto no tenía sentido en nuestro escenario y eran restos de una implementación antigua, decidimos quitarlo directamente, ya investigaremos más adelante si nos encontramos con esta necesidad.

Timeout!

Una vez con la BBDD limpia pensamos que ya lo teníamos todo y pusimos en marcha el procedimiento de migración de nuevo. Era sólo cuestión de esperar que subiera todo el contenido y empezar a trabajar. El problema con una BBDD con muchos datos es que el procedimiento del SSMS no está demasiado refinado todavía y lo más probable es que tengamos algún timeout cuando se están subiendo tablas con grandes cantidades de datos.

Si recordáis el asistente del principio del artículo, el paquete BACPAC se genera en la carpeta %temp% de nuestra máquina.

Antes de perder demasiado tiempo en buscar valores de la configuración para aumentar el tiempo de expiración, debemos saber que podemos subir el paquete BACPAC al Azure Storage y acabar el procedimiento de migración desde el portal de Azure.

Una herramienta gratuita que he encontrado para subir paquetes al storage es: http://azurestorageexplorer.codeplex.com/

Una vez subido el paquete a un blob de nuestra cuenta de Azure Storage sólo nos queda poner en marcha la importación. Desde el nuevo portal todavía no está la opción que explican aquí, así que tuvimos que volver al portal antiguo en el que encontramos todas las funcionalidades. Una vez en el portal es muy sencillo, vamos al apartado de BBDD y en el ribbon seleccionamos “import”:

Para la importación nos pedirá los mismos datos que nos pedía el asistente, además de la url y clave de acceso del Blob donde hemos guardado el archivo BACPAC.

Con todos estos datos se creará una tarea de importación, que podremos ir vigilando por si nos falla en algún momento.

Entre fallo y fallo

Si el proceso nos falla por algún motivo y tenemos que volver a empezar, es muy probable que tengamos que borrar la base de datos del servidor de SQL Azure que hemos creado. Como es un proceso en batch, es posible que la BBDD esté bloqueada mientras se acaba de generar todo el log del procedimiento fallido y no nos dejará borrar la base de datos hasta pasado un cierto tiempo.
Para que no se nos alargue demasiado el tiempo de espera, si estamos realizando la migración a un servidor sin otras bases de datos, podemos eliminar el servidor completo y volver a crearlo.

Otras herramientas y enlaces útiles

Azure queues y el mensaje One of the request inputs is out of range

Las colas en Windows Azure son muy útiles para escalar tareas que se pueden dejar en segundo plano, habitualmente desde un rol Web enviamos trabajos a Worker Roles utilizando colas.

Queue

Durante el inicio de los roles se suele abrir la cola, se comprueba que exista y si no existe la creamos. Son unas pocas líneas:

var storageAccount = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");
var queueStorage = storageAccount.CreateCloudQueueClient();
var queue = queueStorage.GetQueueReference(nombreDeCola);
queue.CreateIfNotExist();

No es un código demasiado complicado, pero hoy me he pasado un buen rato devanándome los sesos porque me daba un error 400 con el siguiente mensaje:

One of the request inputs is out of range

Sin más.

Tras trastear un poco y comparar con otras colas me he dado cuenta que el nombre de la cola contenía alguna letra mayúscula y eso rompe las reglas de nombres en colas (y también en blobs).

Se agradecería un mensaje de error más descriptivo 🙂

Más recursos útiles para Windows Phone 7

Microsoft Patterns & PracticesHace unos días hicimos unos laboratorios de Windows Azure para Windows Phone. Para los que os picó el gusanillo del cloud, los chicos de Patterns & Practices han creado una guía llena de documentos y ejemplos que nos ayudarán a hacer aplicaciones de WP7 con tecnología cloud. Se puede descargar del codeplex o podemos leerla online en el msdn.
Es una guía muy interesante aunque no vayamos a usar Azure, pues ya sabéis que los servicios WCF que hacemos en Azure son casi los mismos que los que podemos hacer en casa.

Y para compensar tanto post de programación hoy os pongo un enlace para para diseñadores y “devigners”, la escuela de diseño de Microsoft incluye un montón de tutoriales nuevos para Windows Phone 7:

Tutoriales de Windows Phone 7 para diseñadores

http://www.microsoft.com/design/toolbox/school/tutorials.aspx

¡Que los disfrutéis!

Herramientas de Windows Azure para Windows Phone 7

Hacía tiempo que no hablaba en el blog de Windows Azure y hoy es para anunciar que se acaba de publicar la primera versión del Windows Azure Toolkit for Windows Phone 7.

Cada vez que explico el modelo de aplicación del teléfono hablo del cloud y de cómo el teléfono está pensado para utilizar los datos en la nube más que para almacenar los datos en él.

Ahora, gracias este nuevo conjunto de herramientas, ejemplos y documentación nos será mucho más fácil usar los servicios de Azure para la autentificación de usuarios, el almacenamiento de datos, etc…

Las herramientas nos instalarán dos nuevas plantillas de solución en Visual Studio 2010: Windows Phone 7 Cloud Application y Windows Phone 7 Empty Cloud Application.
Windows Phone 7 Cloud Application

El proyecto acaba de empezar y ya están pensando en añadir mucha más funcionalidad. Podéis obtener más información en el blog de Wade Wegner.