Weblogs Código

Variable not found

Crear proyectos usando versiones específicas del SDK de .NET Core

abril 24, 2019 04:59

.NET CoreAl hilo del post de ayer, uno de los problemas que vamos a encontrar cuando instalemos versiones preliminares del SDK de .NET Core es que éstas se establecen como las versiones por defecto para el equipo.

Por ejemplo, si instalamos en nuestro equipo la preview del SDK 3.0, a partir de ese momento todos los comandos de la CLI se ejecutarán utilizando esta versión preliminar, como cuando creamos un nuevo proyecto usando dotnet new; en este caso, el proyecto se construirá usando la plantilla proporcionada por la versión más actual del framework (aunque sea preview), lo cual puede resultar molesto en algunas ocasiones.

En este post vamos a ver cómo el propio SDK dispone de mecanismos que nos permiten seleccionar una versión específica, para lo cual trataremos varios comandos útiles de la CLI.

¿Qué versión del SDK estamos utilizando actualmente?

Sencillo: basta con ejecutar el comando dotnet --version. En mi caso, como podéis observar, se trata de una preview de .NET Core 3:
C:\>dotnet --version
3.0.100-preview3-010431

C:\>_

¿Y qué versiones del SDK tenemos instaladas en el equipo?

Esta información podemos obtenerla ejecutando el comando dotnet --list-sdks, como se muestra seguidamente:
C:\>dotnet --list-sdks
1.1.11 [C:\Program Files\dotnet\sdk]
2.1.202 [C:\Program Files\dotnet\sdk]
2.1.503 [C:\Program Files\dotnet\sdk]
2.1.504 [C:\Program Files\dotnet\sdk]
2.1.505 [C:\Program Files\dotnet\sdk]
2.1.602 [C:\Program Files\dotnet\sdk]
2.2.102 [C:\Program Files\dotnet\sdk]
2.2.105 [C:\Program Files\dotnet\sdk]
2.2.202 [C:\Program Files\dotnet\sdk]
2.2.203 [C:\Program Files\dotnet\sdk]
3.0.100-preview3-010431 [C:\Program Files\dotnet\sdk]

C:\>_

¿Cómo creamos un proyecto usando una versión específica del SDK?

Imaginemos que por alguna razón queremos crear un proyecto de consola para .NET Core 1. No es posible hacerlo de forma directa pues, por defecto, los nuevos proyectos serán creados usando la versión más reciente del SDK (en mi caso la preview de .NET Core 3).

Pues bien, lo primero que debemos hacer es crear un archivo llamado global.json en la carpeta raíz del proyecto. Este archivo, cuyo contenido veremos a continuación, permite indicar la versión del SDK que será utilizada en todas las carpetas que se encuentre por debajo en la estructura de directorios.

Podemos crear un global.json a mano (eso sólo un archivo de texto), o bien usando el comando dotnet new global.json especificando la versión del SDK que queremos utilizar que, obviamente, debe ser una de las que tenemos instaladas en el equipo:
C:\test>dotnet new global.json --sdk-version 1.1.11
The template "global.json file" was created successfully.

C:\test>type global.json
{
"sdk": {
"version": "1.1.11"
}
}

C:\test>_
Ahora ya podemos crear el proyecto usando el conocido comando dotnet new, que usará la versión del SDK indicada en global.json:
C:\test>dotnet new console
The template "Console Application" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on C:\test\test.csproj...
Restoring packages for C:\test\test.csproj...
Generating MSBuild file C:\test\obj\test.csproj.nuget.g.props.
Generating MSBuild file C:\test\obj\test.csproj.nuget.g.targets.
Restore completed in 712,13 ms for C:\test\test.csproj.

Restore succeeded.

C:\test>_
Para comprobar que el proyecto ha sido creado correctamente, podemos consultar el archivo .csproj, donde veremos las referencias a las versiones correctas del framework, lo que indica que la plantilla utilizada para crearlo ha sido la apropiada:
C:\test>type test.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp1.1</TargetFramework>
</PropertyGroup>
</Project>

C:\test>_

Publicado en: www.variablenotfound.com.

» Leer más, comentarios, etc...

Fixed Buffer

“Terraformando” nuestra infraestructura desde Azure Pipelines

abril 23, 2019 08:20

La imagen muestra los logos de terraform, azure pipelines y azure cloud

En la última entrada, hablamos sobre Terraform como herramienta para para gestionar nuestra infraestructura mediante código. También hemos hablado hace poco sobre el despliegue continuo desde Azure Pipeline con ARM. Como pudimos comprobar, trabajar con Terraform nos facilita la vida respecto a lo que es el trabajo con ARM directamente. Hoy vamos a unir las dos cosas utilizando Terraform desde Azure Pipelines.

En su momento comentábamos que una de las grandes ventajas de Terraform es que mantiene el estado de los recursos. Esto es algo muy útil y que con trabajo local no supone ningún problema, pero eso se nos queda corto si trabajamos desde el agente de Azure Pipelines. Esto es porque cuando el agente termine va a borrar los recursos, y esto incluye el fichero de estado del despliegue. Para solucionar este “inconveniente” y persistir el estado entre despliegues, vamos a utilizar un Azure Storage donde guardar el fichero.

Una vez aclarado esto, ¡vamos a crear una WebApp en Linux y conectarla a una base de datos Sql Server!

Codificando la infraestructura

Para poder generar la infraestructura y conectarla entre si sin revisión manual, vamos a utilizar la potencia que nos ofrece Terraform para relacionar recursos y así añadir datos como la cadena de conexión. En GitHub está el proyecto completo, dividido en secciones para facilitar su mantenimiento, y utilizando la posibilidad de modular partes para crear la Web App y el Sql Server.

Utilizando estas ventajas, vamos a generar la cadena de conexión de la base de datos utilizando los datos de salida y se lo vamos a pasar a la configuración de la webapp. Para eso, vamos a crear una salida en el módulo de sql server donde vamos a generar la cadena de conexión:

output "conection_string_value" {
  description = "Sql Server ConnectionString"
  value = "Server=${azurerm_sql_server.sqlServer.fully_qualified_domain_name};Initial Catalog=${azurerm_sql_database.sqlServerDb.name};User Id=${var.SQL_ADMIN_ID};Password=${var.SQL_PASSWORD};"
}

Con esto, vamos a poder utilizarla más adelante:

module "linuxwebapp" {
  //...
  conection_string_value = "${module.sqlserver.conection_string_value}"
}

El código de la infraestructura es fácil de seguir, pero aun así recomiendo echarle un ojo en profundidad para entender el concepto anterior.

Creando la integración continua

Para poder desplegar Terraform desde Azure Pipelines, el primer paso es crear el pipeline de integración. De esto hablamos hace algún tiempo en una entrada sobre CI, pero esta vez vamos a utilizar la interfaz gráfica en vez de yml. Como se pueden ver, los pasos son los mismos que cuando utilizábamos yml:

La imagen muestra el pipeline de integración

Aquí solo hay un pequeño cambio respecto a lo que vimos anteriormente, y es que tenemos que meter los ficheros de Terraform al artefacto para poder utilizarlos más adelante:

La imagen señala los campos que hay que rellenar en la tarea de copiar archivos

Para configurar esta nueva tarea, basta con decirle el nombre de la carpeta donde está el código Terraform que queremos desplegar, ponerle el filtro de selección (** para seleccionarlo todo), y por último crear una carpeta en el directorio del artefacto y pegar los ficheros Terraform en él.

El hecho de utilizar la interfaz gráfica para la integración es solo por cambiar, pero para el despliegue solo se puede utilizar la interfaz gráfica de momento.

Ejecutar Terraform desde Azure Pipelines

Una vez que tenemos la integración lista, vamos a crear una Release para que despliegue la infraestructura y el proyecto. Para ahorrarnos trabajo, vamos a utilizar el Task “Terraform Build & Release Tasks“, así que vamos a instalarlo en el pipeline:

La imagen muestra el botón para instalar las Task  de Terraform en Azure Pipelines

Una vez que lo tenemos instalado, vamos a crear el pipeline donde instalaremos Terraform, lo inicializaremos y aplicaremos para desplegar los recursos, y por último publicaremos la web:

La imagen muestra el pipeline de release

Al igual que hacíamos con ARM, lo ideal sería tener diferentes ranuras donde tengamos las diferentes etapas de dev, pre, pro, etc.

Además, vamos a necesitar diferentes variables, que vamos a registrar también en el pipeline, para eso, vamos a la pestaña “Variables”:

La imagen señala la pestaña "Variables"

Y vamos a registrar las variables que necesitamos para nuestro Terraform:

La imagen muestra las variables del pipeline

Por convención, el pipeline le pasa directamente las variables que coincidan y empiecen por TF_VAR a Terraform siempre que no sean secretos. Esto es algo a tener en cuenta para evitarnos un comando apply larguísimo donde le pasemos muchísimas variables.

Volviendo al pipeline, lo primero que tenemos que hacer es instalar Terraform, para eso, vamos a utilizar la Task “Terraform Installer” y le vamos a indicar la versión de Terraform que queremos utilizar:

La imagen muestra donde indicar la versión

El siguiente paso, es configurar una Task de tipo “Terraform CLI” para ejecutar el comando init:

La imagen muestra la interfaz de la Task Terraform CLI

Dentro de esta Task, vamos a seleccionar el comando “init”, y vamos a indicar la ruta donde está el código Terraform, por último, vamos a seleccionar el tipo de backend, que como dijimos al principio, será en Azure Storage, por lo tanto, seleccionamos “azurerm”. Esto nos permite ampliar la configuración pulsando sobre “AzureRM Backend Configuration”, y así indicarle los datos de configuración:

La imagen muestra la configuración del backend en el Task

Con esto listo, el último paso con Terraform es crear una tercera Task de tipo “Terraform CLI”. Esta vez vamos a elegir el tipo de comando “apply”, le vamos a indicar la ruta a donde están los ficheros de código Terrafom, y por último le vamos a indicar las opciones. Entre las opciones, vamos a indicarle “-auto-approve” para que no pida confirmación antes de desplegar los cambios, y le vamos a pasar todas las variables de tipo “secrets” mediante “-var VARIABLE=VARIABLE_PIPELINE” (recordemos que las demás variables se le pasan por convención):

Un ejemplo de las opciones utilizadas es:

-auto-approve -var AZURE_SUBSCRIPTION_ID=$(TF_VAR_AZURE_SUBSCRIPTION_ID) -var AZURE_CLIENT_ID=$(TF_VAR_AZURE_CLIENT_ID) -var AZURE_CLIENT_SECRET=$(TF_VAR_AZURE_CLIENT_SECRET) -var AZURE_TENANT_ID=$(TF_VAR_AZURE_TENANT_ID) -var SQL_PASSWORD=$(TF_VAR_SQL_PASSWORD)

Con esto, y si todo ha ido bien, ya vamos a conseguir desplegar los recursos en Azure. Para desplegar la Web, solo nos queda añadir un Task de tipo “Azure App Service Deploy” tal cual hicimos en el caso de ARM para desplegar la web.

Tras lanzar una release, dentro de nuestro portal en azure podremos encontrar algo como esto:

La imagen muestra los recursos de Azure deplegados mediante Terraform y Azure Pipelines

Para este ejemplo, hemos utilizado el template de ASP NET Core y le hemos añadido que ejecute las migraciones de la base de datos al iniciar, para que la web este lista para funcionar directamente.

Como siempre, he dejado el código fuente completo (tanto Terraform como la Web) en GitHub.

Conclusión

Como hemos podido comprobar, Terraform es una herramienta muy potente que nos facilita mucho la vida manejando infraestructura como código (IaC), además de permitirnos trabajar con múltiples proveedores. Podemos integrar perfectamente Terraform y Azure Pipelines, o cualquier otro servicio CI/CD utilizando los comandos directamente, por lo que es algo que vale la pena revisar y conocer.

**La entrada “Terraformando” nuestra infraestructura desde Azure Pipelines se publicó primero en Fixed Buffer.**

» Leer más, comentarios, etc...

Variable not found

Cómo probar C#8 desde Visual Studio 2019 o CLI

abril 23, 2019 06:36

Visual Studio 2019Como sabréis, C# 8 está aún en preview, pero hace ya tiempo que estamos recibiendo información sobre las novedades que traerá la nueva versión de nuestro querido lenguaje: rangos, tipos referencia anulables, mejoras en patrones, más usos de using... pinta divertido, sin duda :D

En el futuro iremos comentando las características más interesantes, pero, de momento, este post vamos a dedicarlo exclusivamente a ver cómo podemos comenzar a probar C# 8 desde nuestro flamante Visual Studio 2019 u otros entornos, como VS Code o incluso la CLI.
Si aún no habéis tenido el ratillo para instalar la última versión de Visual Studio, ya estáis tardando ;D
El único problema es que necesitamos compiladores que entiendan la sintaxis de C# 8, y de momento esto sólo es posible usando la preview de .NET Core 3. Pero vaya, nada que no podamos solucionar en un par de minutos; veamos cómo.

Paso 1: Instalamos la preview de .NET Core 3

Tan simple como ir a la página oficial de descargas, elegir el paquete apropiado e instalarlo en nuestro equipo.

Al finalizar, una ejecución en la línea de comandos de 'dotnet --info' filtrando por el texto "preview" debería mostrar algo como lo siguiente:
C:\>dotnet --info | find "preview"
Version: 3.0.100-preview3-010431
Base Path: C:\dotnet\sdk\3.0.100-preview3-010431\
Version: 3.0.0-preview3-27503-5
3.0.100-preview3-010431 [C:\dotnet\sdk]
Microsoft.AspNetCore.App 3.0.0-preview3-19153-02 [C:\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.NETCore.App 3.0.0-preview3-27503-5 [C:\dotnet\shared\Microsoft.NETCore.App]
Microsoft.WindowsDesktop.App 3.0.0-preview3-27504-2 [C:\dotnet\shared\Microsoft.WindowsDesktop.App]

C:\>_
Si queremos probar C# 8 desde fuera de Visual Studio, no sería necesario realizar nada más. Por ejemplo, bastaría con crear un proyecto con dotnet new console e introducir un código como el siguiente, donde vemos que se utilizan rangos, una de las nuevas características del lenguaje:
static void Main(string[] args)
{
var arr = new[] { 1, 2, 3, 4, 5, 6 };
var ouch = arr[2..4];
Console.WriteLine(ouch[0] + ouch[1]); // 3 + 4 = 7
}
Ojo: esto significa que a partir del momento de su instalación, esta versión preliminar será vuestro SDK por defecto. Tenedlo en cuenta si vais a crear nuevos proyectos desde la línea de comandos.

Paso 2: Activamos el uso de previews de .NET Core en Visual Studio 2019

Por defecto, el entorno de desarrollo sólo utilizará las versiones finales de .NET Core que tengáis instaladas en vuestro equipo. Para usar una versión preliminar hay que indicárselo explícitamente accediendo a la opción Herramientas>Opciones del menú principal, buscando la configuración de .NET Core y marcando la opción "Usar versiones preliminares de .NET Core SDK":

Activar el uso de previews de .NET Core
En mi caso, este settings no fue aplicado hasta que reinicié Visual Studio. Por tanto, quizás no esté de más que lo hagáis antes de continuar, para asegurar que todo irá bien. Total, con VS2019 el arranque es más rápido que nunca ;)
A partir de este momento, Visual Studio podrá crear y manejar proyectos .NET Core 3. Para comprobarlo de forma sencilla, sólo tenemos que crear un proyecto de consola desde el entorno e introducir en él un código como el anterior para comprobar que compila y se ejecuta sin problema :)

También, a la hora de crear proyectos ASP.NET Core, aparecerá esta versión en el selector:

ASP.NET Core 3 en Visual Studio 2019

Ya en siguientes artículos iremos viendo las nuevas incorporaciones al lenguaje, que prometen traernos diversión a raudales :D

Publicado en: www.variablenotfound.com.

» Leer más, comentarios, etc...

Koalite

Unpopular opinions: software development edition

abril 22, 2019 05:06

A lo largo del tiempo he ido escribiendo bastantes posts en los que intentaba analizar desde un punto de vista (más o menos) racional varios aspectos relacionados con el desarrollo de software. En este post aprovecharé para repasar algunos de ellos. En realidad, nada es ni tan blanco ni tan negro y en los posts enlazados podréis ver un poco mejor la gama de grises.

Quizá no sean exactamente opiniones impopulares. Más bien podríamos decir que son opiniones controvertidas sobre temas que pueden ser examinados con cierto detalle y en los que podemos (y debemos) replantearnos nuestro punto de vista periódicamente.

Veamos algunas.

Utilizar métodos estáticos está bien

En los diseños orientados a objetos se tiende a demonizar su uso, pero los métodos estáticos no son más que funciones, y como tales son muy útiles. Bien utilizados ofrecen alternativas interesantes a la hora de diseñar y pueden simplificar muchos escenarios de testing (en contra de lo que se suele pensar).

Utilizar tipos primitivos ayuda a escribir código más mantenible

Evitar modelar todo con strings o decimals es una cosa, pero introducir tipos específicos para cada concepto que te encuentras provoca que tengas que reimplementar muchas operaciones básicas que están disponibles para los tipos estándar.

Es preferible evitar utilizar clases

Si tu lenguaje lo permite, es mejor intentar diseñar utilizando estructuras más simples (valores, funciones o módulos) antes que empezar a pensar en clases. Los objetos acoplan estado y operaciones, pero además tienen un ciclo de vida que hay considerar. Si no vas a necesitar varias instancias de una clase, probablemente no necesites una clase.

No pasa nada porque (algunas) clases sean grandes

Partir la funcionalidad de un sistema en una miríada de pequeñas clases muy focalizadas y (supuestamente) reutilizables está muy bien, pero puede hacer más difícil entender como funciona todo que tener un código más líneal (y procedural) empaquetado en menos clases. Además, si necesitas garantizar que se mantienen ciertos invariantes en partes críticas de una aplicación, a veces sólo te quedan las clases como mecanismo para encapsular información y evitar usos indebidos de la misma.

En muchos casos utilizar TDD no tiene sentido

Pese a que suene a anatema en determinados ámbitos, hay muchas situaciones en las que utilizar TDD no ayuda a mejorar el diseño ni el proceso de desarrollo. Ni siquiera tener tests automatizados debería ser algo a perseguir sin pararse a pensar antes qué pretendes obtener con los tests.

La inyección de dependencias no debe ser la opción por defecto

Diseñar un sistema para permitir realizar inyección de dependencias en todos los puntos posibles añade una complejidad a la hora de utilizarlo que, sencillamente, no compensa si al final vas a tener una única implementación de cada dependencia. Refactoriza el código para usar inyección de dependencias cuando lo necesites, pero no lo hagas de forma especulativa.

Los ORMs son muy útiles (y no por evitarte escribir SQL)

Los ORMs tienen muchos detractores e implican varios sacrificios (curva de aprendizaje, abstracciones incompletas, …). Pese a todo ofrecen muchas ventajas a la hora de modelar, especialmente gracias a cosas como el Identity Map, la persistencia por alcance o el polimorfismo, que van mucho más allá de evitarte escribir SQL (algo de lo que nunca te puedes pretender aislar por completo).

Los repositorios son importantes (aunque utilices un ORM)

Cuando se utiliza un ORM existe cierta tendencia a despreciar el uso de repositorios por considerarlos una parte “ya cubierta” por el propio ORM. Utilizar repositorios es más que encapsular llamadas a una capa de persitencia. Utilizar repositorios permite establecer un lenguaje común sobre el tipo de operaciones que es pueden realizar sobre cada entidad y ayudar así a proteger los invariantes de tu dominio.

Dejar ficheros en una carpeta es un mecanismo de integración válido

Es indudable que se trata de un sistema prehistórico y con muchos inconvenientes frente a utilizar alternativas más modernas como APIs Web o colas de mensajes. Sin embargo, la facilidad para implementarlo, usarlo y comprobar los datos que se mueven cuando hay errores son ventajas que no hay despreciar.

Hay que comentar el código

El código limpio de los auténticos artesanos ágiles es completamente autoexplicativo y con un nivel de abstracción tal que permite leerlo como si fuese una novela. Sí, vale, me lo creo. Pero lo que no puede leerse es la mente de quien lo programó para saber por qué tomó unas deciciones y no otras. Puedes llevar esa documentación fuera del código (mensajes de commits, pull requests, etc.), pero documentar el porqué de las cosas junto al propio código hace que sea más sencillo consultarlo.

Asumir deuda técnica es algo perfectamente razonable

“Como clean-coder quiero que mi código sea limpio y mantenible para que así…” Todo eso está muy bien, pero nunca hay que olvidar que el código hasta que no está en producción no sirve para nada. Saber gestionar la deuda técnica, ser consciente de cuándo hay que asumirla y de cuándo hay que empezar a pagarla es clave para el desarrollo de software.

Subir las dependencias al repositorio de código es la mejor forma de crear compilaciones reproducibles

Incluir dentro del repositorio de código fuente las dependencias de terceros está considerado una mala práctica en muchos entornos, pero te permite independizarte por completo de servicios de terceros durante el proceso de compilación y garantizar que las compilaciones son realmente reproducibles.

Menos despliegue continuo y más despliegue de calidad

Queda muy bien presumir de que puedes hacer 400 despliegues en producción diarios y que tu tiempo para resolver una incidencia es menor de 4 minutos desde que la implementas hasta que llega a los usuarios, pero como usuario preferiría no haberme encontrado con la incidencia directamente. Hace falta dedicar más tiempo a diseñar y probar antes de empezar a utilizar a tus usuarios como testers gratuitos.

Conclusión

En el fondo, no hay tantas cosas en el desarrollo de software que sean realmente tan opinables.

No es cuestión de “yo tengo derecho a opinar como quiera y toda opinión es respetable”. Es cuestión de analizar los pros y los contras de todo sin empezar a dar por sentado cosas sólo porque estén consideras buenas prácticas o las llevemos haciendo toda la vida.

Estos análisis no pueden realizarse de forma completamente abstracta y generalista y, ahí sí, entra en juego el contexto (personal, tecnológico, de equipo, de negocio, …) en el que nos encontramos a la hora de tomar decisiones.

No hay posts relacionados.

» Leer más, comentarios, etc...

Blog Bitix

Estrategias de despliegue para microservicios con Nomad

abril 18, 2019 09:30

Nomad
HashiCorp

El ciclo de vida de una aplicación no consiste solo en desarrollarla, incluye también su puesta en producción o despliegue en un entorno de pruebas, pero también una vez la aplicación está desplegada en algún momento será necesario actualizarla con una nueva versión.

Las aplicaciones monolíticas tienen otros problemas pero en el aspecto de despliegue es sencillo ya que solo hay una aplicación, basta con desplegar la nueva versión. En una aplicación con arquitectura de microservicios es un reto mayor debido a que hay múltiples aplicaciones.

En cualquiera de ellas puede darse el caso de que para ganar en escalabilidad o para aumentar la disponibilidad o tolerancia a fallos es posible que haya varias instancias, las cuales han de ser actualizadas con el requisito si es necesario de que el servicio no deje de prestar su servicio, es decir que el despliegue no suponga una caída del servicio.

Hay varias estrategias para desplegar una nueva versión de una aplicación:

  • Rolling update: actualizar todas las instancias de forma progresiva. Una vez se termina de actualizar una se espera un tiempo y se actualiza la siguiente hasta que todas estén actualizadas.
  • Blue/Green: manteniendo en funcionamiento las instancias con la versión antigua se crea el mismo número de instancias con la nueva versión y se redirige tráfico hacia ellas. Una vez se ha comprobado que la nueva versión funciona correctamente se promociona la nueva versión y se eliminan las instancias de con la versión antigua. Esta estrategia permite volver a la versión anterior rápidamente si se detecta algún problema.
  • Canary: se siguen manteniendo las instancias con la versión antigua, a diferencia de la estrategia blue/green se crea un número menor de instancias con la versión nueva que el número de instancias con la versión antigua. Una vez comprobado que la nueva versión es correcta se promociona la nueva versión y se actualizan todas las instancias restantes mediante rolling update a la nueva versión. También permite volver a la versión antigua si se detecta algún problema.

Docker Swarm permite la estrategia de despliegue rolling update sin embargo las estrategias blue/green y canary son interesante para tratar de que un error en una versión nueva no afecte al funcionamiento de la aplicación y obligue hacer un rollback que posiblemente tarde más tiempo durante el cual el servicio funcionará con el defecto descubierto. Nomad permite despliegues con las estrategias blue/gree y canary.

Para actualizar un servicio en Nomad basta con modificar la definición del job y enviarlo a Nomad, y este se encarga de orquestar la actualización en las instancias según la estrategia de despliegue configurada. En este caso se actualiza la versión de nginx de la versión nginx:stable-alpine a nginx:alpine usando una estrategia rolling update para las cinco instancias del servicio.

La estrategia de despliegue en Nomad se define en la sección de configuración update. El parámetro min_healthy_time es el tiempo que se espera cuando se hace un rolling update para considerar una instancia como sana y continuar la actualización con la siguiente, max_parallel indica el número de instancias que se migran al mismo tiempo. El parámetro canary indica el número de instancias que se crean en las estrategias blue/green y canary, en la primera el número de instancias coincidirá con el parámetro canary que indica el número de instancias de un servicio. Nomad con los parámetros health_check, min_healthy_time, healthy_deadline, progress_deadline, stagger y auto_revert se puede poner unos límites para considerar válido un despliegue y en caso de no serlo realizar un rollback de forma autmática.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
job "nginx" {
datacenters = ["localhost"]
type = "service"
update {
min_healthy_time = "15s"
max_parallel = 1
}
group "services" {
count = 5
task "nginx" {
driver = "docker"
config {
image = "nginx:stable-alpine"
port_map {
http = 80
}
}
resources {
memory = 1024 # MB
network {
port "http" {}
}
}
}
}
}
1
2
$ nomad job run nginx.nomad

En el caso de los despliegues blue/green y canary una vez comprobado que la versión de los nuevos servicios funcionan correctamente se promocionan y actualizan el resto de instancias en el caso de canary o se detienen las instancias antiguas en el caso de blue/green.

1
2
$ nomad job promote nginx

Desde la línea de comandos se puede observar el estado del servicio y el proceso de actualización, el primero es el estado previo a realizar el despliegue, el segundo durante el proceso de actualización con rolling update y el tercero una vez finalizado el proceso de despliegue y marcado como exitoso en el que todas las instancias han pasado de la versión 0 a la 1.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
$ nomad job status nginx
ID = nginx
Name = nginx
Submit Date = 2019-04-18T19:13:07+02:00
Type = service
Priority = 50
Datacenters = localhost
Status = running
Periodic = false
Parameterized = false
Summary
Task Group Queued Starting Running Failed Complete Lost
services 0 0 5 0 0 0
Latest Deployment
ID = 81f57b6d
Status = successful
Description = Deployment completed successfully
Deployed
Task Group Desired Placed Healthy Unhealthy Progress Deadline
services 5 5 5 0 2019-04-18T19:23:23+02:00
Allocations
ID Node ID Task Group Version Desired Status Created Modified
3747eb07 d18851d5 services 0 run running 29s ago 13s ago
500575e9 d18851d5 services 0 run running 29s ago 13s ago
c8094cf3 d18851d5 services 0 run running 29s ago 13s ago
ea58300c d18851d5 services 0 run running 29s ago 13s ago
ead6d23f d18851d5 services 0 run running 29s ago 13s ago
$ nomad alloc status 3747eb07
ID = 3747eb07
Eval ID = 781fb5f2
Name = nginx.services[3]
Node ID = d18851d5
Job ID = nginx
Job Version = 0
Client Status = running
Client Description = Tasks are running
Desired Status = run
Desired Description = <none>
Created = 56s ago
Modified = 40s ago
Deployment ID = 81f57b6d
Deployment Health = healthy
Task "nginx" is "running"
Task Resources
CPU Memory Disk Addresses
0/100 MHz 788 KiB/1.0 GiB 300 MiB http: 127.0.0.1:29939
Task Events:
Started At = 2019-04-18T17:13:08Z
Finished At = N/A
Total Restarts = 0
Last Restart = N/A
Recent Events:
Time Type Description
2019-04-18T19:13:08+02:00 Started Task started by client
2019-04-18T19:13:07+02:00 Task Setup Building Task Directory
2019-04-18T19:13:07+02:00 Received Task received by client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$ nomad job status nginx
ID = nginx
Name = nginx
Submit Date = 2019-04-18T19:17:54+02:00
Type = service
Priority = 50
Datacenters = localhost
Status = running
Periodic = false
Parameterized = false
Summary
Task Group Queued Starting Running Failed Complete Lost
services 0 0 5 0 5 0
Latest Deployment
ID = ba20066a
Status = successful
Description = Deployment completed successfully
Deployed
Task Group Desired Placed Healthy Unhealthy Progress Deadline
services 5 5 5 0 2019-04-18T19:29:19+02:00
Allocations
ID Node ID Task Group Version Desired Status Created Modified
fabcf384 d18851d5 services 1 run running 2m36s ago 2m20s ago
ccb57008 d18851d5 services 1 run running 2m53s ago 2m37s ago
b06c743d d18851d5 services 1 run running 3m10s ago 2m54s ago
56733896 d18851d5 services 1 run running 3m28s ago 3m12s ago
71c8bb5b d18851d5 services 1 run running 3m45s ago 3m29s ago
500575e9 d18851d5 services 0 stop complete 8m31s ago 3m44s ago
c8094cf3 d18851d5 services 0 stop complete 8m31s ago 3m10s ago
3747eb07 d18851d5 services 0 stop complete 8m31s ago 2m53s ago
ea58300c d18851d5 services 0 stop complete 8m31s ago 3m27s ago
ead6d23f d18851d5 services 0 stop complete 8m31s ago 2m35s ago
<noscript><a href="https://asciinema.org/a/241669" target="_blank"><img src="https://asciinema.org/a/241669.png" width="734" /></a></noscript> Progreso del despliegue rolling update en Nomad

El proceso de despliegue también se puede monitorizar desde la interfaz web que ofrece Nomad.

Progreso del despliegue rolling update en Nomad

En este ejemplo los servicios están en contenedores docker, también se observa que la versión de los contenedores en ejecución pasan de la versión stable-alpine a alpine.

1
2
3
4
5
6
7
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
476d2063b64b nginx:stable-alpine "nginx -g 'daemon of…" About a minute ago Up About a minute 127.0.0.1:29939->80/tcp, 127.0.0.1:29939->80/udp nginx-3747eb07-f9da-a9f5-0720-1b2314baac12
e0a533348f44 nginx:stable-alpine "nginx -g 'daemon of…" About a minute ago Up About a minute 127.0.0.1:21085->80/tcp, 127.0.0.1:21085->80/udp nginx-ea58300c-c4a1-cc3d-46a0-d5b30c276ede
4da9babdd549 nginx:stable-alpine "nginx -g 'daemon of…" About a minute ago Up About a minute 127.0.0.1:30061->80/tcp, 127.0.0.1:30061->80/udp nginx-c8094cf3-5c3c-eaaa-1bcf-9368100bb0ef
068c6db6a86c nginx:stable-alpine "nginx -g 'daemon of…" About a minute ago Up About a minute 127.0.0.1:26606->80/tcp, 127.0.0.1:26606->80/udp nginx-ead6d23f-abdd-8b33-7b61-c2ad64dede5c
19190e778a5a nginx:stable-alpine "nginx -g 'daemon of…" About a minute ago Up About a minute 127.0.0.1:29835->80/tcp, 127.0.0.1:29835->80/udp nginx-500575e9-62ce-868a-f142-869475aacde5
1
2
3
4
5
6
7
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
03faf7ed4467 nginx:alpine "nginx -g 'daemon of…" About a minute ago Up About a minute 127.0.0.1:27212->80/tcp, 127.0.0.1:27212->80/udp nginx-fabcf384-5675-fe7f-4c61-5fa7385c54d2
4185977fbddb nginx:alpine "nginx -g 'daemon of…" 2 minutes ago Up 2 minutes 127.0.0.1:26686->80/tcp, 127.0.0.1:26686->80/udp nginx-ccb57008-e426-1684-fd39-97cb0f3b51f7
c6586d22406e nginx:alpine "nginx -g 'daemon of…" 2 minutes ago Up 2 minutes 127.0.0.1:23508->80/tcp, 127.0.0.1:23508->80/udp nginx-b06c743d-d2b3-0be3-d82b-762184dda4ec
046253c1972b nginx:alpine "nginx -g 'daemon of…" 2 minutes ago Up 2 minutes 127.0.0.1:31800->80/tcp, 127.0.0.1:31800->80/udp nginx-56733896-ab20-ab8f-36c9-ac1d13b0f1a2
88788b4133ea nginx:alpine "nginx -g 'daemon of…" 3 minutes ago Up 3 minutes 127.0.0.1:30844->80/tcp, 127.0.0.1:30844->80/udp nginx-71c8bb5b-a23b-7edc-cbb3-f9d0b6bdffe6

Nomad y Consul se inician con los siguientes comandos en modo desarrollo comentados en el artículo Introducción a Nomad para gestionar aplicaciones y microservicios.

1
2
$ consul agent -dev -datacenter localhost
$ sudo nomad agent -dev -dc localhost

» Leer más, comentarios, etc...

Blog Bitix

Introducción a Nomad para gestionar aplicaciones y microservicios

abril 17, 2019 09:00

Nomad
Consul
HashiCorp

En las arquitecturas de aplicaciones basadas en microservicios cada microservicio o simplemente servicio es una aplicación distinta e independiente, son varias aplicaciones que hay que gestionar y desplegar de forma individual o de forma coordinada.

Dado su número hay que automatizar todas las tareas para tratar de conseguir la menor intervención manual, ningún proceso manual si es posible. Para que no sean inmanejables han de tratarse como ganado de forma masiva en vez de como animales de compañía que requieran atención individual para continuar generando valor en un proyecto con el tiempo disponible en vez de dedicarlo a tareas que no lo aportan.

Dado que cada microservicio puede emplear una tecnología diferente es necesario algo que permita tratarlos a todos por igual, esta tecnología son los contenedores que hacen un papel similar al que hacen en el transporte de mercancías en barcos.

Hay varias tecnologías para orquestar o gestionar los microservicios y crear clusters de máquinas en las que desplegarlos, una de ellas es Docker Swarm sencilla e integrada con Docker pero no con tantas funcionalidades como otra de las populares que es Kubernetes y para usarlo en una máquina local minikube, ofrece mas funcionalidad pero añade una complejidad significativa que para algunos casos de uso no compensa además requiere mas tiempo para dominarla. Una solución intermedia conservando la sencillez pero con mas funcionalidad es Nomad de HashiCorp. Otra de sus características destacadas es que el cluster de Nomad puede estar formado en diferentes centros de datos y proveedores de la nube al mismo tiempo, por ejemplo en AWS, GCP entre otros o centros de datos híbridos en la nube y propios. En la sección Nomad vs. Other Software de su documentación se compara con otras opciones.

Nomad es distribuido, con alta disponibilidad y escalable a cientos de nodos en múltiples centros de datos y regiones. No es una solución completa por si sola pero se integra con otras. No ofrece descubrimiento de servicios pero se integra muy bien con Consul. No es un balanceador de carga pero se integra con Nginx, Fabio, Traefik y HAproxy automatizando su configuración desde Consul. No integra un gestor de secretos pero se integra con Vault. No soporta escalado y no integra de por sí varias de estas funcionalidades como Kubernetes pero es más simple.

Nomad a diferencia Docker no solo puede gestionar contenedores docker sino también tareas del sistema y otras como máquinas virtuales qemu o contenedores con rkt, a diferencia de Kubernetes es mucho mas sencilla pero conservando funcionalidad suficiente para muchos casos de uso. Nomad requiere de otra de las herramientas de HashiCorp que es Consul para el registro y descubrimiento y para la configuración del cluster, también se integra con otras de sus herramientas como Vault para guardar cifrados datos sensibles como contraseñas y certificados. Con Connect es capaz de proporcionar conexión TLS con autenticación mutua de forma transparente para los servicios.

Los jobs son la unidad de trabajo que contienen la definición de los servicios, se definen en un archivo de configuración donde los elementos son el nombre, los grupos de tareas, las tareas y en cada tarea el driver que usa dependiendo del cual se proporciona la configuración apropiada. El driver determina como es gestionada el tipo de tarea puede ser un conteendor de docker, un proceso del sistema, una máquina virtual de qemu o un contenedor rkt, a diferencia de Docker Swarm que solo puede gestionar contenedores docker. Se pueden configurar variables de entorno, memoria asignada a cada tarea, propiedades de red y CPU.

Nomad y Nomad cada uno son un binario ejecutable sin ninguna otra dependencia. Basta con descargarlos e incluirlos en el path del sistema. En este ejemplo hay definido un job compuesto por una tarea de un contenedor docker de nginx configurado en un puerto aleatorio y con 1 GB de memoria para cada una de las dos instancias del servicio. Dado que lo usa es necesario instalar Docker.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
job "nginx" {
datacenters = ["localhost"]
type = "service"
update {
stagger = "30s"
canary = 2
max_parallel = 2
}
group "services" {
count = 2
task "nginx" {
driver = "docker"
config {
image = "nginx:alpine"
port_map {
http = 80
}
}
resources {
memory = 1024 # MB
network {
port "http" {}
}
}
}
}
}

Nomad y Nomad se ejecutan con los siguientes comandos en modo desarrollo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ consul agent -dev -datacenter localhost
==> Starting Consul agent...
==> Consul agent running!
Version: 'v1.4.4'
Node ID: '1934d5b2-0f3f-ffdd-8378-7ab8a6207bb1'
Node name: 'archlinux'
Datacenter: 'localhost' (Segment: '<all>')
Server: true (Bootstrap: false)
Client Addr: [127.0.0.1] (HTTP: 8500, HTTPS: -1, gRPC: 8502, DNS: 8600)
Cluster Addr: 127.0.0.1 (LAN: 8301, WAN: 8302)
Encrypt: Gossip: false, TLS-Outgoing: false, TLS-Incoming: false
$ sudo nomad agent -dev -dc localhost
[sudo] password for picodotdev:
==> No configuration files loaded
==> Starting Nomad agent...
==> Nomad agent configuration:
Advertise Addrs: HTTP: 127.0.0.1:4646; RPC: 127.0.0.1:4647; Serf: 127.0.0.1:4648
Bind Addrs: HTTP: 127.0.0.1:4646; RPC: 127.0.0.1:4647; Serf: 127.0.0.1:4648
Client: true
Log Level: DEBUG
Region: global (DC: localhost)
Server: true
Version: 0.9.0

Iniciados se pueden enviar jobs y ver su estado, dirección y puerto asignado así como los logs generados.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
$ nomad job plan nginx.nomad
$ nomad job run nginx.nomad
==> Monitoring evaluation "b2b07746"
Evaluation triggered by job "nginx"
Allocation "60f3d102" created: node "58823be1", group "services"
Allocation "6e12ae8f" created: node "58823be1", group "services"
Evaluation within deployment: "0dddab79"
Allocation "6e12ae8f" status changed: "pending" -> "running" (Tasks are running)
Allocation "60f3d102" status changed: "pending" -> "running" (Tasks are running)
Evaluation status changed: "pending" -> "complete"
==> Evaluation "b2b07746" finished with status "complete"
$ nomad job status nginx
ID = nginx
Name = nginx
Submit Date = 2019-04-14T13:31:53+02:00
Type = service
Priority = 50
Datacenters = localhost
Status = running
Periodic = false
Parameterized = false
Summary
Task Group Queued Starting Running Failed Complete Lost
services 0 0 2 0 0 0
Latest Deployment
ID = 0dddab79
Status = running
Description = Deployment is running
Deployed
Task Group Desired Placed Healthy Unhealthy Progress Deadline
services 2 2 2 0 2019-04-14T13:42:04+02:00
Allocations
ID Node ID Task Group Version Desired Status Created Modified
60f3d102 58823be1 services 0 run running 12s ago 1s ago
6e12ae8f 58823be1 services 0 run running 12s ago 1s ago
$ nomad alloc status 60f3d102
ID = 60f3d102
Eval ID = b2b07746
Name = nginx.services[1]
Node ID = 58823be1
Job ID = nginx
Job Version = 0
Client Status = running
Client Description = Tasks are running
Desired Status = run
Desired Description = <none>
Created = 56s ago
Modified = 45s ago
Deployment ID = 0dddab79
Deployment Health = healthy
Task "nginx" is "running"
Task Resources
CPU Memory Disk Addresses
0/100 MHz 820 KiB/1.0 GiB 300 MiB http: 127.0.0.1:28421
Task Events:
Started At = 2019-04-14T11:31:54Z
Finished At = N/A
Total Restarts = 0
Last Restart = N/A
Recent Events:
Time Type Description
2019-04-14T13:31:54+02:00 Started Task started by client
2019-04-14T13:31:53+02:00 Task Setup Building Task Directory
2019-04-14T13:31:53+02:00 Received Task received by client
$ nomad alloc logs 60f3d102
172.17.0.1 - - [14/Apr/2019:11:33:17 +0000] "GET / HTTP/1.1" 200 612 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0" "-"
172.17.0.1 - - [14/Apr/2019:11:33:17 +0000] "GET /favicon.ico HTTP/1.1" 404 154 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0" "-"

También poseen una interfaz web integrada en la que consultar la misma información, la de Nomad está en el puerto 8500 y la de Nomad en el 4646 donde ver el estado de los jobs y el progreso de los despliegues.

Interfaces web de Consul y Nomad

Nomad permite varias estrategias para actualizar los jobs a una nueva versión de un servicio, basta modificar la configuración del job, volverlo a enviar a Nomad y este se encarga de actualizar las instancias siguiendo la estrategia rolling, blue/green o canary definida en el job, pero eso lo muestro mas detalladamente en otro artículo.

La documentación de Nomad, Nomad y otros productos de HashiCorp dedicados a la infraestructura en la nube esta muy bien explicada y detallada, este artículo solo es un resumen de las partes básicas para conocer como empezar a usarlo. En el siguiente vídeo se hace una pequeña explicación y demostración.

» Leer más, comentarios, etc...

Picando Código

Gleam – Nuevo lenguaje de programación funcional estáticamente tipado en BEAM

abril 17, 2019 12:30

El pasado lunes 15 de abril se publicó la primera versión de Gleam (0.1): Gleam es un lenguaje de programación funcional estáticamente tipado diseñado para escribir sistemas concurrentes mantenibles y escalables. Compila a Erlang y tiene interoperabilidad directa con otros lenguajes de BEAM (la máquina virtual de Erlang) como Erlang, Elixir y LFE.

Obviamente es un lenguaje bastante nuevo, así que no está listo para usar en producción. Está interesante y ya se puede probar, programar alguna cosa divertida y aprender a usarlo. El código fuente está disponible en GitHub bajo licencia Apache 2.0, y chat del proyecto se encuentra en IRC en #gleam-lang de Freenode. Aplaudo el uso de IRC para proyectos de Software Libre en vez de Slack 👏
¡El compilador está escrito en Rust! Y si bien no conozco Rust, parece que saca algo de inspiración de ese lenguaje en la sintaxis y le veo también algunas cosas de Elixir.

Principios de Gleam

Ser seguro

Un sistema de tipados expresivo inspirado por la familia de lenguajes ML nos ayuda a encontrar y prevenir bugs en tiempo de compilación, mucho antes que el código llegue a los usuarios.

Para los problemas que no pueden ser resueltos con el sistema de tipos (como que le caiga un rayo al servidor) el runtime Erlang/OTP provee mecanismos bien testeados para manejar fallas.

Ser amigable

Perseguir bugs puede ser estresante así que la realimentación del compilador debe ayudar y ser lo más clara posible. Queremos pasar más tiempo trabajando en nuestra aplicación y menos tiempo buscando errores tipográficos o descifrando mensajes de error crípticos.

Como comunidad queremos ser amigables también. Personas de todos los orígenes, géneros y niveles de experiencia son bienvenidas y deben recibir respeto por igual.

Ser performante

El runtime Erlang/OTP es conocido por su velocidad y habilidad para escalar, permitiendo que organizaciones como WhatsApp y Ericsson manejen cantidades masivas de tráfico de manera segura con baja latencia. Gleam debería aprovechar por completo el runtime y ser tan rápido como otros lenguajes de la BEAM como Erlang y Elixir.

Ser un buen ciudadano

Gleam facilita el uso de código escrito en otros lenguajes BEAM como Erlang, Elixir y LFE, así que hay un ecosistema rico de herramientas y librerías para que los usuarios de Gleam aprovechen.

A cambio los usuarios de lenguajes BEAM deberían poder aprovechar Gleam, ya sea usando librerías escritas en Gleam de manera transparente, o agregando módulos Gleam a sus proyectos existentes con mínimo trabajo.

Instalación

Para instalarlo, debemos seguir esta guía, Necesitamos tener Rust, Erlang y rebar3 instalados de antemano, clonar el código con git, y compilar el compilador. El soporte para editores de texto todavía está un poco en pañales, pero ya hay modos para Emacs, Vim y Visual Studio Code.

Empezando a programar

Al ser un lenguaje tan nuevo, todavía no es un proceso tan directo crear un proyecto. De todas formas sólo hay que seguir estos pasos. El compilador puede trabajar con proyectos generados con rebar3, una herramienta de builds de Erlang.

Los tipos básicos de Gleam son String, Bool (True || && False), Int (enteros) y Float (decimales). Los valores pueden nombrarse con let para ser reusados después, pero los valores contenidos son inmutables:

let x = 1
let y = x
let x = 2

x // => 2
y // => 1

Tiene Tuplas, colecciones ordenadas de tamaño fijo con elementos que pueden ser de distintos tipos:

{"Texto", 2, True}

Y acá me empiezo a acordar de Elixir, podemos extraer los valores de la tupla así:

> valores = {"Texto", 2, True}
{"Texto", 2, True}
> {a, b, c} = valores
{"Texto", 2, True}
> a
"Texto"
> b
2
)> c
True

Otra estructura de datos son las Listas: Colecciones ordenadas de elementos del mismo tipo:

[7, 8, 9]
// Y para agregar nuevos valores, como en Elixir, se usa:
> ["N" | ["a", "n", "d", "o"]] 
["N", "a", "n", "d", "o"]

Por último están los Mapas, colecciones de nombre y valores que pueden ser de cualquier tipo:

{
  nombre = "Nicanor",
  edad = 20,
}

Y se puede acceder al valor con la sintaxis `mapa.nombre_del_campo`:

let persona = { nombre = "Nicanor", edad = 20 }
let nombre = persona.nombre

nombre // => "Nicanor"

Para actualizar o agregar valores a un mapa se usa la sintaxis: { mapa | nombre_del_campo = valor }:

let persona1 = { nombre = "Marcela", edad = 22 }
let persona2 = { persona1 | edad = 23, idioma: "español" }

persona1 // { nombre = "Marcela", edad = 22 }
persona2 // { nombre = "Marcela", edad = 23, idioma: "español" }

Los tipos del mapa dependen de los nombres y tipos de los campos. El compilador lleva un registro de los campos y valores de cada mapa y presenta un error en tiempo de compilación en caso de querer usar un campo que no exista o tenga el tipo incorrecto.

Las funciones con nombre se declaran con pub fn:

pub fn sumar(x, y) {
  x + y
}

pub fn multiplicar(x, y) {
  x * y
}

Y como son valores de primera clase se pueden asignar a variables, pasar a funciones y todo lo demás que se puede hacer con cualquier otro tipo de datos:

pub fn dos_veces(f, x) {
  f(f(x))
} // Wooooo!

También tiene funciones anónimas con una sintaxis similar:

pub fn ejecutar() {
  let sumar = fn(x, y) { x + y }
  sumar(1, 2)
}

No puedo evitar las comparaciones con Elixir, pero es a lo que me hace acuerdo… Hay una sintaxis corta para crear funciones anónimas que toman un argumento y lo pasan a otra función. Y esto se usa generalmente con pipes para crear una serie de transformaciones de datos:

pub fn sumar(x, y) {
  x + y
}

pub fn ejecutar() {
  // esto equivale a sumar(sumar(sumar(1,2), 4), 6)
  1
  |> sumar(_, 2)
  |> sumar(_, 4)
  |> sumar(_, 6)
}

Vistas las funciones otra cosa interesante es pasarles mapas como parámetros. El lenguaje apunta a ser muy permisivo con esto, y muestra un ejemplo de cómo funciona:

fn numero_siguiente(mapa) {
   mapa.numero + 1
}

El tipo de la función es fn({ a | numero = Int}) -> Int. La a en este caso puede ser “cualquier otro campo”, así que la función se puede llamar con cualquier mapa siempre y cuando tenga el campo numero con un valor del tipo Int.

let articulo = { nombre: "tenedor", numero: 17 }
let nintendo = { nombre: "gameboy", numero: 4 }
let fernando = { nombre: "Fernando", edad: 33 }

numero_siguiente(articulo) // => 18
numero_siguiente(nintendo) // => 5
numero_siguiente(fernando) // => Compile time error! No numero field

La expresión case se usa como estructura de control por medio de la técnica “pattern matching” (ya escribí algo sobre Pattern Matching en el blog antes). Cómo se usa:

case numero {
| 0 -> "Cero"
| 1 -> "Uno"
| 2 -> "Dos"
| n -> "Otro número" // machea todo lo que no sea lo anterior
}

Como alternativa al if else de otros lenguajes, hace pattern matching con los valores Bool:

case alguna_condicion {
| True -> "Es verdadera"
| False -> "Es falsa"
}

La expresión case retorna un valor por lo que podemos asignarle el resultado a una variable. El pattern matching me resulta una cosa mágica y feliz, pero debe ser porque no estoy acostumbrado a usarlo.

El lenguaje también cuenta con Enums, y el tipo Bool está definido como uno:

enum Bool =
  | True
  | False

Una variante que muestra como ejemplo para extraer valores es:

enum User =
  | LoggedIn(String)
  | Guest

let diego = LoggedIn("Diego")
let leticia = LoggedIn("Leticia")
let visitor = Guest

Los enums también pueden ser “patternmacheados” para asignarle nombres a distintas variantes y podemos usarlos en un let.
🤯

Por último, al ser un lenguaje de la BEAM, podemos usar funciones de otros lenguajes directamente ¡Santa interoperabilidad Batman! Al ser lenguajes distintos, el compilador no puede determinar el tipo de las funciones, así que es la responsabilidad del programador en esos casos de hacer las cosas bien o veremos hermosas explosiones en nuestra aplicación. Pero llamar funciones de otros lenguajes es bastante sencillo:

// Llamando a la función uniform del módulo rand de Erlang:
pub external fn random_float() -> Float = "rand" "uniform"

// Llamando a IO.inspect de Elixir:
pub external fn inspect(a) -> a = "Elixir.IO" "inspect"

Como si fuera poco, también podemos importar tipos externos.

Conclusión

Si bien está en una etapa muy temprana de desarrollo, Gleam me resultó un lenguaje súper interesante que iré siguiendo con atención. Al ser tan nuevo, todavía hay mucho por implementar, así que resulta un buen aprendizaje y bastante interesante ir viendo cómo se desarrollan nuevas funcionalidades.

Elixir, Crystal y ahora Gleam son los lenguajes que me interesaría usar más aparte de Ruby, y en algún momento me voy a poner a leer también sobre Rust.

Si quieren aprender más de Gleam, pueden visitar su sitio web.

» Leer más, comentarios, etc...

Variable not found

Enlaces interesantes 358

abril 15, 2019 06:55

Enlaces interesantesComo entretenimiento para la Semana Santa, ahí van los enlaces recopilados durante los últimos siete días. Como siempre, espero que os resulten interesantes :)

Por si te lo perdiste...

.NET / .NET Core

ASP.NET / ASP.NET Core

Azure / Cloud

Conceptos / Patrones / Buenas prácticas

Data

Machine learning / IA / Bots

Web / HTML / CSS / Javascript

Visual Studio / Complementos / Herramientas

Xamarin

Otros

Publicado en Variable not found.

» Leer más, comentarios, etc...

Bitácora de Javier Gutiérrez Chamorro (Guti)

Necromancer’s DOS Navigator

abril 13, 2019 08:01

DOS Navigator comienza en 1991, un gestor de archivos para DOS que proporciona una cantidad de funciones que representan un salgo enorme en la tecnología de la época. Su evolución continua hasta 1998 cuando presentan la versión 1.50, aunque desde la 1.35 de 1995 decidieron dedicar sus esfuerzos al cliente de correo para Windows The […]

La entrada Necromancer’s DOS Navigator aparece primero en Bitácora de Javier Gutiérrez Chamorro (Guti).

» Leer más, comentarios, etc...

Blog Bitix

Aumentar el tamaño del identificativo de la cookie de sesión de Tomcat o Spring Session

abril 12, 2019 05:00

Tomcat
Java
Spring Boot

El protocolo HTTP es un protocolo sin estado que quiere decir que entre las peticiones no se comparte estado ni se recuerda ningún dato. Por otro lado las cookies es pequeño conjunto de datos que son almacenados en el cliente y son enviados en cada petición que se hace a un sitio web, cada sitio web mantiene su propia colección de cookies, dos sitios distintos no comparten sus cookies. Para mantener estado entre dos peticiones se hace uso de las cookies.

En Java los servidores web envían al cliente una cookie con simplemente un identificativo de la sesión, el estado se suele mantener en el servidor en memoria, en almacenamiento de disco o persistido en una base de datos como Redis. El identificativo de la sesión por defecto usando un contenedor de servlets como Tomcat tiene una longitud de 16 bytes que codificados en hexadecimal da lugar a 32 caracteres o 128 bits. Para aumentar la seguridad por si alguien intenta adivinar el identificativo de cualquier usuario que tenga sesión iniciada por fuerza bruta de casualidad es posible aumentar el número de caracteres para identificar la cookie de sesión. La clase de la API que lo permite en Tomcat es Manager.

Según Insufficient Session-ID Length un identificativo con solo 64 bits (32 de entropía) un atacante haciendo 1000 intentos por segundo y 10000 sesiones válidas tarda solo 7,15 minutos en obtener una sesión válida (32 bit = 4294967296 / 10.000 = 429496, a 1000 intentos por segundo da 429 segundos o 7,15 minutos). Con 128 bits el tiempo crece a 292 años haciendo 10000 intentos por segundo y teniendo 100000 sesiones válidas, pero podría reducirse si el número de intentos por segundo aumentase o sesiones aumentase.

Los datos se guardan en el servidor y la cookie con el identicativo de sesión no ocupa mucho aún pasando de 32 caracteres hexadecimales a una cifra mayor como 128, el número de caracteres no es significativo para el rendimiento pero se dificulta en varios órdenes de magnitud la dificultad de adivinar una cookie.

Unsando Spring Boot y Tomcat basta con usar la clase Manager para cambiar el valor por defecto de longitud de la sesión.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package io.github.picodotdev.springsession;
...
@SpringBootApplication
@ComponentScan("io.github.picodotdev.springsession")
public class Main {
private static final int SESSION_ID_LENGTH = 64;
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> cookieProcessorCustomizer() {
return (TomcatServletWebServerFactory factory) -> {
factory.addContextCustomizers((Context context) -> {
if (context.getManager() == null) {
context.setManager(new StandardManager());
}
context.getManager().getSessionIdGenerator().setSessionIdLength(SESSION_ID_LENGTH);
});
};
}
...
public static void main(String[] args) throws Exception {
SpringApplication.run(Main.class, args);
}
}

Persistiendo la sesión en Redis con Spring Sesion por defecto el identificativo de la sesión es generado a partir de un UUID, el identificativo de la sesión tiene el mismo valor por defecto de 128 bits pero para cambiar la longitud hay que proporcionar una clase que cambia el comportamiento.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package io.github.picodotdev.springsession;
...
@SpringBootApplication
@EnableRedisHttpSession
@ComponentScan("io.github.picodotdev.springsession")
public class Main {
private static final int SESSION_ID_LENGTH = 64;
@Bean
public DefaultCookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setUseBase64Encoding(false);
return cookieSerializer;
}
@Bean
@Primary
public RedisOperationsSessionRepository defaultSessionRepository(RedisOperations<Object, Object> sessionRedisOperations) {
return new DefaultRedisOperationSessionRespository(sessionRedisOperations);
}
public static void main(String[] args) throws Exception {
SpringApplication.run(Main.class, args);
}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package org.springframework.session.data.redis;
...
public class DefaultRedisOperationSessionRespository extends RedisOperationsSessionRepository {
private static final String HEX_CHARACTERS = "0123456789ABCDEF";
private static final int MAX_INACTIVE_INTERVAL_MINUTES = 30;
private static final int SESSION_ID_LENGTH = 127;
private SecureRandom randomGenerator;
public DefaultRedisOperationSessionRespository(RedisOperations<Object, Object> sessionRedisOperations) {
super(sessionRedisOperations);
}
@Override
public RedisOperationsSessionRepository.RedisSession createSession() {
if (randomGenerator == null) {
randomGenerator = new SecureRandom();
}
byte[] bytes = new byte[SESSION_ID_LENGTH];
randomGenerator.nextBytes(bytes);
String id = getHex(bytes);
RedisOperationsSessionRepository.RedisSession redisSession = new RedisOperationsSessionRepository.RedisSession(new MapSession(id));
redisSession.setMaxInactiveInterval(Duration.ofMinutes(MAX_INACTIVE_INTERVAL_MINUTES));
return redisSession;
}
private String getHex(byte [] bytes) {
if (bytes == null) {
return null;
}
StringBuilder hex = new StringBuilder( 2 * bytes.length);
for (byte b : bytes) {
hex.append(HEX_CHARACTERS.charAt((b & 0xF0) >> 4)).append(HEX_CHARACTERS.charAt((b & 0x0F)));
}
return hex.toString();
}
}
Longitud del identificativo de sesión de 64 bytes o 128 caracteres hexadecimales
1
2
D2C631033F477F9A3111F40CFDBB83DA041BC7EB4C7CD3F824349945E9CA73E660FE3E0D4DC75A685E9255F7F3C538AC1CE07ED055547CA379BA2CB7B8A52516

Un libro dedicado a la seguridad muy bueno que he leído es Iron-Clad Java Applications, tiene montón de detalles dedicados a la seguridad de las aplicaciones web sean seguras, incluido como este dedicado a la longitud de los identificativos de la sesión.

Una clave asimétrica considerada segura puedes ser de 2048 bits pero se puede generar una de hasta 8192 bits con el mismo esfuerzo lo que aumenta la seguridad de forma exponencial ante un ataque de fuerza bruta que con el aumento de la capacidad de cómputo y en el futuro puede ser viable. El tiempo de cómputo requerido por usar una clave de mayor tamaño no creo que sea significativo para la mayoría de los casos pero igualmente la seguridad aumenta.

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradlew run.

» Leer más, comentarios, etc...

Picando Código

Spring – nueva película animada hecha con Blender

abril 10, 2019 02:59

Spring es el 12° proyecto de la iniciativa Open Movies de Blender:

Spring es la historia de una niña pastora y su perro, que enfrentan espíritus antiguos para continuar el ciclo de la vida. Este poético y visualmente maravilloso cortometraje fue escrito y dirigido por Andy Goralczyk, inspirado por su niñez en las montañas de Alemania.

Blender es software de gráficos 3D libre usado para crear animaciones, efectos especiales, arte, modelos imprimibles 3D, aplicaciones y videojuegos. Mediante la Fundación Blender, se han producido varios proyectos de alta calidad con el objetivo de demostrar, validar y mejorar las capacidades del software.
Varios de ellos ya son bastante conocidos:
Elephants Dream, Big Buck Bunny, Sintel, Tears of Steel, Caminandes Gran Dilamma, Cosmos Laundromat, Glass Half, Caminandes Llamigos, Agent 327 Operation Barbershop, Daily Dweebs y Hero.

El equipo de producción de Spring usó la versión de desarrollo de Blender 2.80 durante toda la producción, incluso antes que el software estuviera oficialmente en Beta. Como con todas las películas anteriores de Blender, el proceso de producción entero y todos los archivos fuente están siendo compartido en la plataforma Blender Cloud. Si acceden al enlace, pueden ver una línea de tiempo de varias semanas de desarrollo y materiales que se van compartiendo como artwork, storyboards y demás materiales de producción. También se incluyen tutoriales, videos del proceso y mucho material más.

La imagen del corto es espectacular y el resultado final es excelente. Les recomiendo mirarlo. Y visiten este enlace si quieren ver más películas del estilo libres, gratuitas y totalmente legales de mirar como y dónde quieran.

Formato: película animada 3D, 7:44 minutos. sonido 5.1. Sin diálogos. Apta para audiencias de 6 años para arriba (PG).
Todo lo que se ve fue hecho con Blender, GIMP y Krita.

» Leer más, comentarios, etc...

Arragonán

Diseño incremental de software a partir de las interacciones (parte 1)

abril 10, 2019 12:00

Este post viene a cuento de que me quedé sin hacer la charla de la Bilbostack 2019 por andar pachucho durante varias semanas, teniendo que cancelar el compromiso al no haberme recuperado a los pocos días del evento. Me quedó el contenido a medio preparar y tenía la espinita clavada de compartir mis ideas sobre el tema.

Dibujo representando la autosimilaridad de Lean Startup, ATDD y TDD

¿Qué narices es eso del diseño incremental?

Ahora que “agile” (nótese el entrecomillado ;)) es mainstream, parece que se va asumiendo en muchos sitios que hacer software de forma iterativa ayuda a hacer mejor software. Cada cierto tiempo hay entregas que permiten validar si se va por un camino correcto o no, comprobar malos entendidos, ver problemas que han surgido, adaptabilidad a cambios…

Lo que bajo mi percepción no es tan mainstream es la parte de incremental, cuesta más hacer vertical slicing. En mi opinión esto suele ocurrir por falta de habilidades o conocimiento de prácticas relacionadas con Extreme Programming, como son test first o el propio diseño incremental.

Aunque siempre hay que hacer un esfuerzo inicial para poder sentar algunas bases respecto a la visión del producto, qué problemas o necesidades se quiere resolver, la arquitectura de alto nivel, etc.; muchas veces se peca de querer bajarlo demasiado en detalle desde el inicio, pretendiendo tener una foto de cómo va a ser la solución y adivinar el futuro.

No olvidemos que, cuando desarrollamos un software, hoy sabemos menos que mañana. En cada iteración que pasa, más aprendemos sobre el negocio, conocemos mejor las herramientas y la tecnología, intentamos mejorar como equipo, etc.

Así que una cosa que debemos tener clara es que, si queremos que nuestro software dure, tenemos que enfocarnos en diseñar software que acepte cambios. Aceptar la incertidumbre frente a tratar de adivinar problemas futuros.

¿Y lo de diseñar a partir de las interacciones?

Pues básicamente enfocar el diseño de software desde cómo se va a utilizar, ya sean personas que interactúan a través de una UI u otros sistemas que lo hacen usando algún tipo de API.

Seguramente resulta familiar el término API first, que es una aproximación similar. Empezar definiendo cómo se expone el software partir de un contrato e ir haciendo Outside-In hacia la implementación y modelado más interno.

En el caso del diseño a partir de las interacciones nos abstraemos de los detalles de un API. Anteponemos la definición de la interacción a la del API ya que esta no es nada más que un detalle técnico de cómo se expone la interacción al mundo.

Y cuidado: No podemos caer en la trampa de definir pobremente esas interacciones, sin descubrir el negocio que hay debajo y acabar pensando que sólo son CRUDs. Nunca es un CRUD: reglas de negocio, efectos colaterales, integraciones con terceros…

Descubrimiento de producto

Antes de identificar las interacciones, un trabajo importante es lo que se suele llamar descubrimiento de producto. Algunas cosas que necesitamos saber: objetivos de negocio del producto, quiénes son los usuarios y sus motivaciones, dependencias técnicas u organizativas, posibles restricciones, identificar a los stakeholders…

Es un tiempo dedicado a analizar y entender los problemas para poder enfocar mejor la solución, ya que no hay nada peor que descubrir que hemos contruido algo que nadie quiere.

Hay multitud de actividades que se pueden usar dependiendo del contexto (producto, equipo, negocio…): business model canvas, value proposition canvas, entrevistas, user personas, mapa de stakeholders, mapa de empatía, benchmarking de producto, customer journey map… o usar un paquete de este tipo de actividades como design sprint, agile inception deck o lean inception.

Estas actividades, muy estilo design thinking, se han quedado relegadas habitualmente a la gente de UX, negocio o gestión de producto, ya que a los técnicos típicamente no nos ha preocupado demasiado o no se nos ha incluido.

Participar en estas actividades nos ayuda a entender infinitamente mejor las necesidades durante la evolución del desarrollo del producto, además de facilitarnos el planteamiento inicial de la arquitectura de software a alto nivel. Y es que hacer diseño incremental no quiere decir que no hagamos un trabajo previo.

Formalización del backlog

Para poder organizarnos y priorizar debidamente necesitamos tener disponible algún tipo de backlog de producto, de modo que lo oportuno es hacer un volcado de product backlog items (sin preocuparnos demasiado del tamaño) como complemento a las diferentes actividades de descubrimiento que se hayan podido realizar.

A mi personalmente me gusta el terminar visibilizándolo en un formato estilo user story map. Aunque siempre he omitido la parte de journey, simplemente me gusta verlo organizado en dos dimensiones: funcionalidad/tema y prioridad agrupada en swimlanes.

Me parece muy interesante organizar las swimlanes como must, could y nice to have, al menos como organización incial o de cara a implementar un MVP. Otras veces, teniendo razonablemente claro el alcance final, las he utilizado directamente para definir releases o entregas parciales tentativas.

Refinamiento de historias de usuario

Ya con la prioridad, sabemos por donde hay que ponernos a aterrizar esos product backlog items y formalizarlo en una o varias historias de usuario. Recordemos que una historia de usuario no es sólo completar una plantilla, debería ser siempre un artefacto que sirve como recordatorio de (o excusa para) una conversación sobre ella y que además debería tener unos criterios de aceptación específicos.

Además de en las dailies o conversaciones más informales entre los miembros del equipo, habitualmente estas conversaciones deberían ocurrir más intensamente en algún tipo de sesión de planning, de la que deberíamos extraer los criterios de aceptación de las historias de usuario en las que vayamos a trabajar próximamente. Esas sesiones pueden contener ejercicios de estimación como planning poker o de refinamiento como example mapping.

Personalmente me gusta bastante el example mapping porque ayuda a estructurar las conversaciones, desgranando las historias de usuario en forma de reglas, dudas y ejemplos que ilustran las reglas.

Specification by Example

Las reglas forman parte de los criterios de aceptación de una historia, mientras que los ejemplos nos marcan la especificación y nos sirven como primer paso para hacer Specification by Example (prefiero ese nombre que Behaviour-Driven Development) con el que dirigir el diseño incremental de las iteraciones.

Como herramienta para hacer Specification by Example suelo utilizar cucumber, de modo que las especificaciones terminan en el típico formato gherkin Given/When/Then.

Evidentemente se pueden utilizar otras herramientas para esto, pero ¿por qué cucumber?:

  • Al separar las especificaciones como texto plano, permite colaborar fácilmente con perfiles no técnicos para elaborarlo o validarlo.
  • Ayuda forzarse a utilizar lenguaje ubicuo que deberíamos haber ido desarrollando (quizá incluso mantengamos un glosario de términos).
  • Queda como fuente de verdad y sirve como documentación viva. Si una funcionalidad cambia, quedará reflejado el cambio.
  • Facilita hacer ATDD separando claramente el ciclo de especificación del de desarrollo.
  • Permite implementar tests automáticos a distintos niveles. Pudiendo hacer tests sólo de la lógica de negocio, desde la UI, a través de un API…

A partir de las historias de usuario y de las especificaciones con ejemplos tendremos identificados los casos de uso o interacciones, el alcance bien definido y habremos trabajado el lenguaje ubicuo.

Este tipo de prácticas hacen más eficiente el proceso de desarrollo, aumentando la calidad del producto y evitando retrabajo a causa de malosentendidos.

Próximamente

Espero escribir al menos un post más, tratando la parte más técnica a través de un ejemplo práctico sobre:

  • Implementar tests automáticos desde especificaciones gherkin con cucumber.
  • Artefactos tácticos de Domain Driven Desing.
  • TDD Outside-In, empezando desde las interacciones o use cases.
  • Arquitectura hexagonal/clean, escapando de frameworks.

Otros recursos

En forma de cursos, posts y videos relacionados que tengo repartidos por ahí:

» Leer más, comentarios, etc...

Picando Código

Kingdom Rush: Rift in Time ¡Juego de tablero de Kingdom Rush en Kickstarter!

abril 09, 2019 07:46

La famosa saga de juegos Tower Defense, Kingdom Rush, llega al mundo de los juegos de tablero. Mediante una campaña en Kickstarter, Kingdom Rush: Rift in Time alcanzó la meta inicial de USD 20.000 en apenas media hora.

Kingdom Rush: Rift In Time

Los encargados de trasladar Kingdom Rush de los píxeles a cartón, plástico y madera son el equipo del estudio Lucky Duck Games, que ya tiene experiencia con varios títulos. El producto se describe como un juego de puzzle, completamente cooperativo, dirigido por una campaña y de proporciones épicas. Si ya han disfrutado Kingdom Rush, van a ver que se trata de una adaptación fiel a juego de mesa. En la página de Kickstarter pueden encontrar varias instrucciones y videos que explican cómo se juega. Pero la idea sigue siendo defender nuestra base de hordas de enemigos con torres, ¡pero cooperativo!

De 1 a 4 jugadores, para mayores de 12 años y con partidas de entre 60 y 90 minutos. Se incluyen varios tableros con caminos como para generar el escenario, fichas de corazones y diamantes, cartas para las hordas y torres, las formas a lo Tetris para atacar desde las torres, marcadores para torres, soldados y ¡héroes! En principio serán 4 héroes y dos jefes (figuras de plástico 😍).

Va a estar traducido a Inglés y Francés, y podemos elegir una de esas dos versiones si lo compramos durante la campaña. Pero también va a incluir un manual digital para Español, Italiano y Polaco. Aseguran que la mayor parte del contenido del juego es independiente del lenguaje. Así que con aprenderse las reglas, seguramente no va a ser necesario volver al manual.

Como varias campañas en Kickstarter, hay distintas metas que se van a ir alcanzando a medida que más gente invierta en el proyecto. Las metas son más héroes, hordas, torres y con suerte escenarios. Al momento de escribir esto, ya se superaron los USD 110.000 de recaudación, por lo que se desbloquearon al menos 7 de estas metas. El paquete básico, denominado Knight Pledge, cuesta USD 59 e incluye todo lo necesario para jugar + todas las metas que se desbloqueen. Pero también podemos comprar el King Pledge por USD 79, una edición de lujo y limitada que nunca va a estar disponible para comprar en otro lado. Incluye mejoras al paquete básico y un póster. Por ahora también hay un agregado disponible por USD 19 que incluye un héroe, un jefe, y una campaña nueva.

Me alegra muchísimo ver a Kingdom Rush en formato juego de tablero y ¡no puedo esperar a probarlo!

 

» Leer más, comentarios, etc...

Variable not found

Antipatrones de asincronía en C#

abril 09, 2019 11:09

Manos sobre tecladoHace unas semanas leía el post C# Async Antipatterns de Mark Heath, y encontré en él problemas en la implementación de código asíncrono que, coincidiendo con el autor, creo que son bastante frecuentes.

Como la asincronía ha llegado para quedarse y aún hay desarrolladores que no lo tienen claro del todo, he pensado que sería interesante traducir y republicar aquí el post, por supuesto, con permiso expreso de su autor (thank you, Mark! ;))

¡Vamos allá!

Antipatrones de asincronía en C#

Las palabras clave async y await han hecho un gran trabajo simplificando la forma de escribir código asíncrono en C#, pero, desafortunadamente, no pueden protegernos de hacer las cosas mal. En este artículo me gustaría recoger algunos de los errores de codificación o antipatrones más comunes relativos a asincronía que suelo encontrar en revisiones de código.

1. Olvidar el await

Cuando llamamos a un método que retorna un objeto Task o Task<T>, no deberíamos ignorar su valor de retorno. Esto decir, la mayor parte de las veces deberemos utilizar await para esperar su resultado, aunque hay algunas ocasiones en las que podríamos necesitar almacenar el objeto Task para esperar a su finalización más adelante.

Observad que en el siguiente ejemplo invocamos a Task.Delay(), pero dado que no esperamos su finalización, el mensaje "Después" es mostrado inmediatamente porque la llamada Task.Delay(1000) simplemente retorna un objeto Task que finalizará un segundo más tarde, pero, dado que nadie está esperando a que esta tarea sea completada, la ejecución continúa sin detenerse:
Console.WriteLine("Antes");
Task.Delay(1000);
Console.WriteLine("Después");
Cuando cometemos este error en un método que retorna un objeto Task y está marcado como asíncrono con la palabra clave async, el compilador nos dará un error bastante descriptivo:
Because this call is not awaited, execution of the current method continues before the call is completed. 
Consider applying the 'await' operator to the result of the call.
Sin embargo, en código síncrono o métodos que retornan tareas sin estar marcados como async, el compilador de C# no protestará y nos permitirá hacerlo sin problema. Por tanto, debemos estar atentos para que este error no pase desapercibido.

2. Ignorar tareas asíncronas

A veces nos interesa ignorar deliberadamente el resultado de una llamada asíncrona porque no queremos esperar a que ésta se complete. Por ejemplo, podría tratarse de un tarea de largo tiempo de ejecución que queremos que se realice en segundo plano, mientras en primer plano continuamos con otra cosa.

A menudo veo código como el siguiente:
// Hacer algo en segundo plano - no queremos esperar a que finalice
var ignoredTask = DoSomethingAsync();
El peligro de utilizar este enfoque es que, dado que no esperamos la finalización de la tarea, las excepciones que pudieran ser lanzadas desde DoSomethingAsync() no serían capturadas en ningún momento. En este caso, lo mejor que puede pasar es que no nos enteremos de que la operación en segundo plano falló; lo peor, es que incluso podría tumbar el proceso completo y provocar la finalización de la aplicación.

Por tanto, debemos utilizar esta técnica con mucha precaución, y siempre asegurar que el método invocado gestiona apropiadamente las excepciones. A menudo veo su uso en aplicaciones en la nube, y en muchas ocasiones tiendo a refactorizarlo enviando un mensaje a una cola cuyo handler realiza la tarea en segundo plano de forma totalmente independiente.

3. Utilizar métodos async void

A menudo os encontraréis escribiendo un método síncrono (es decir, un método que no retorna una instancia de Task o Task<T>) en cuyo interior os interesará llamar a un método asíncrono (que retorne un Task). Sin embargo, sabemos que no podemos utilizar la palabra clave await sin marcar el método como async, por lo que tendremos que buscar otras fórmulas. Básicamente hay dos formas de conseguirlo, y las dos son arriesgadas.

La primera es cuando estamos en un método void. En este caso, el compilador de C# nos permitirá añadir async a la firma del método, por lo que podremos utilizar await sin problema:
public async void MyMethod()
{
await DoSomethingAsync();
}
El problema en este caso es que los consumidores de MyMethod() no tienen forma de esperar a que este método finalice su ejecución, pues no tienen acceso al objeto Task que retorna la llamada a DoSomethingAsync(). Por lo tanto, básicamente estaremos ignorando la tarea asíncrona de nuevo.

Sin embargo, hay algunos casos de uso válidos para los métodos async void. El mejor ejemplo lo encontraríamos en los manejadores de eventos de aplicaciones Windows Forms o WPF. Los event handlers retornan siempre void, por lo que no podemos hacer que retornen el objeto Task con la tarea asíncrona.

Por tanto, no hay ningún problema en utilizar un código como el siguiente:
public async void OnButton1Clicked(object sender, EventArgs args)
{
await LoadDataAsync();
// update UI
}
Pero aparte de esto, en la mayoría de los casos es recomendable evitar métodos async void. Si podéis hacer que vuestros métodos retornen Task, deberíais hacerlo.

4. Bloquear tareas con .Result o .Wait()

Otra forma bastante habitual de consumir métodos asíncronos desde métodos síncronos es utilizando la propiedad .Result o llamando a Wait() sobre el objeto Task que representa a la tarea asíncrona.

La propiedad Result espera a que la tarea finalice y retorna su resultado, lo cual a priori puede parecer bastante útil. Podemos utilizarlo como se muestra a continuación:
public void SomeMethod()
{
var customer = GetCustomerByIdAsync(123).Result;
}
Sin embargo, esto genera varios problemas. El primero es que utilizar llamadas como Result o Wait() hace que el hilo de ejecución quede bloqueado y no pueda ser destinado a otros menesteres mientras espera la finalización de la tarea asíncrona. Pero lo que es peor, la mezcla de código asíncrono con llamadas a .Result (o Wait()) abre la puerta a problemas de interbloqueos realmente terribles.

Por lo general, siempre que necesitéis llamar a un código asíncrono desde un método debéis aseguraros de que éste también sea asíncrono. Sin duda, esto puede suponer un cierto esfuerzo adicional y deriva en muchos cambios en cascada para introducir asincronía en toda la cadena de llamadas, sobre todo en una base de código extensa, pero esto es preferible al riesgo de introducir deadlocks.

Puede haber casos en los que no es posible hacer el método asíncrono. Por ejemplo, si queremos invocar un código asíncrono desde el constructor de una clase, esto no será posible, aunque normalmente podremos rediseñar la clase para no necesitarlo.

Por ejemplo, en lugar de esto:
class CustomerHelper
{
private readonly Customer customer;
public CustomerHelper(Guid customerId, ICustomerRepository repo)
{
customer = repo.GetAsync(customerId).Result; // avoid!
}
}
En su lugar, podríamos utilizar una factoría asíncrona para instanciar el objeto:
class CustomerHelper
{
public static async Task<CustomerHelper> CreateAsync(Guid customerId, ICustomerRepository repo)
{
var customer = await repo.GetAsync(customerId);
return new CustomerHelper(customer)
}

private readonly Customer customer;
private CustomerHelper(Customer customer)
{
this.customer = customer;
}
}
Otros escenarios en los que nos encontramos con las manos atadas es cuando implementamos una interfaz de terceros que es síncrona y no puede ser modificada. Por ejemplo, me he encontrado en esta situación al utilizar IDisposable o implementar el atributo ActionFilterAttribute de ASP.NET MVC. En estos casos hay que ser muy creativos para idear una solución, o simplemente asumir que necesitaremos realizar llamadas bloqueantes y prepararnos para introducir muchas llamadas a ConfigureAwait(false) para protegernos de los bloqueos (hablaremos de esto más adelante).

Las buenas noticias son que, en el desarrollo actual, es cada vez más raro encontrar escenarios en los que sea necesario bloquear una tarea. Por ejemplo, desde C# 7.1 podemos declarar métodos async Main para aplicaciones de consola y ASP.NET Core está diseñado con la asincronía en mente desde sus orígenes, mucho más de lo que lo estaba ASP.NET MVC.

5. Mezclar ForEach() con métodos asíncronos

La clase List<T> tiene un "útil" método llamado ForEach(), que permite ejecutar un Action<T> sobre cada elemento de la lista. Si habéis visto algunas de mis ponencias sobre LINQ ya conoceréis mis opiniones en contra de este método, pues fomenta una gran variedad de malas prácticas (podéis leer este artículo para conocer algunas de las razones para evitar el uso de ForEach()).

Pero aparte de esto, una aplicación especialmente peligrosa de ForEach() que he visto es utilizarlo para invocar métodos asíncronos. Por ejemplo, imaginemos que queremos enviar un email a todos los clientes de la siguiente manera:
customers.ForEach(c => SendEmailAsync(c));
¿Cuál es el problema con este código? Pues bien, lo que estamos haciendo es exactamente lo mismo que en el siguiente bucle foreach:
foreach(var c in customers)
{
SendEmailAsync(c); // The return task is ignored
}
Hemos generado un objeto Task por cada cliente, pero no hemos esperado a que ninguna de estas tareas finalice.

A veces he visto desarrolladores que intentan solucionarlo introduciendo async y await en la lambda:
customers.ForEach(async c => await SendEmailAsync(c));
Esto no supone ninguna diferencia. El método ForEach() acepta un Action<T> que retorna void, por lo que básicamente hemos creado un método async void que, por supuesto, es uno de los antipatrones que hemos comentado anteriormente, pues el consumidor no tiene forma de esperar su finalización.

Y entonces, ¿cómo solucionamos esto? Bien, personalmente preferiría sustituir ese código por un bucle foreach explícito:
foreach(var c in customers)
{
await SendEmailAsync(c);
}
Algunos desarrolladores preferirían hacer esto en un método extensor, llamado por ejemplo ForEachAsync(), lo que permitiría escribir código como el siguiente:
await customers.ForEachAsync(async c => await SendEmailAsync(c));
Pero en definitiva, no mezcléis List<T>.Foreach() (o Parallel.Foreach, que de hecho tiene el mismo problema) con métodos asíncronos.

6. Exceso de paralelización

En algunas ocasiones, podemos identificar una serie de tareas que se realizan de forma secuencial y forman parte de un cuello de botella en el rendimiento de nuestras aplicaciones. Por ejemplo, imaginad un código que procesa pedidos secuencialmente como el mostrado a continuación:
foreach(var o in orders)
{
await ProcessOrderAsync(o);
}
Pues bien, a veces encuentro desarrolladores que intentan acelerar este proceso de la siguiente manera:
var tasks = orders.Select(o => ProcessOrderAsync(o)).ToList();
await Task.WhenAll(tasks);
Lo que conseguimos con esto es llamar a ProcessOrderAsync() para cada pedido, y almacenar en una lista cada uno de los objetos Task retornados. Tras ello, esperamos a que todas estas tareas finalicen.

En efecto, este código funciona, pero, ¿qué ocurriría si existieran 10.000 pedidos? Habríamos inundado el thread pool con miles de tareas, quizás evitando que otras tareas del sistema puedan ser realizadas. Además, si ProcessOrderAsync() hiciera a su vez llamadas a otros servicios externos como bases de datos o microservicios, estaríamos saturándolos con un gran volumen de llamadas.

¿Y cuál sería el enfoque correcto en este caso? Bueno, al menos deberíamos considerar limitar el número de tareas concurrentes que pueden llamar a ProcessOrderAsync() al mismo tiempo. En este artículo describí distintas fórmulas para conseguir esto.

Cuando veo este código en una aplicación en la nube, a veces es una señal de que tenemos que introducir algún sistema de mensajería para que la carga pueda ser dividida en bloques y gestionada por lotes desde más de un servidor.

7. Efectos laterales del código no thread safe

Si alguna vez habéis echado un vistazo a la programación funcional (lo que recomiendo que hagáis incluso si no tenéis intención de cambiar de lenguaje), os habréis topado con el concepto de funciones puras. Conceptualmente, las funciones puras son aquellas que no son sensibles a efectos laterales; toman unos datos de entrada y retornan otros de salida, pero no realizan ninguna mutación de estado en su interior. Las funciones puras ofrecen muchos beneficios, incluyendo su inherente seguridad en entornos multiproceso.

A menudo encuentro métodos como el siguiente, donde enviamos una lista o un diccionario a un método que los modifica de alguna forma:
public Task ProcessUserAsync(Guid id, List<User> users)
{
var user = userRepository.GetAsync(id);
// do other stuff with the user
users.Add(user);
}
El problema es que este código es peligroso porque no impide que la lista de usuarios pueda ser modificada al mismo tiempo desde otro hilo de ejecución. A continuación podemos ver el mismo método actualizado para eliminar los posibles efectos laterales sobre la lista:
public Task<User> ProcessUserAsync(Guid id)
{
var user = userRepository.GetAsync(id);
// do other stuff with the user
return user;
}
Así, hemos movido la responsabilidad de añadir el usuario a la lista al consumidor del método, que tendrá muchas más posibilidades de asegurarse de que la lista es accedida únicamente desde un thread.

8. Ausencia de ConfigureAwait(false)

ConfigureAwait() no es un concepto especialmente fácil de comprender para nuevos desarrolladores, pero su correcta utilización puede ser crítica si estáis trabajando sobre una base de código síncrono que utiliza Result o Wait() para esperar la finalización de tareas asíncronas.

Sin entrar en mucho detalle, el significado principal de ConfigureAwait(true) es informar de que quiero que el código continúe ejecutándose en el mismo contexto de sincronización cuando mi tarea haya finalizado.

Por ejemplo, en una aplicación WPF, el contexto de sincronización es el UI thread, y dado que sólo es posible actualizar componentes de la interfaz de usuario desde este hilo, casi siempre tendré que utilizar ConfigureAwait(true) en mi código de UI, como se muestra a continuación:
private async void OnButtonClicked()
{
var data = await File.ReadAllBytesAsync().ConfigureAwait(true);
this.textBoxFileSize.Text =
$"The file is ${data.Length} bytes long"; // needs to be on the UI thread
}
Hoy en día, ConfigureAwait(true) se emplea por defecto en todas partes, por lo que podríamos eliminarlo en el ejemplo anterior y todo seguiría funcionando.

Entonces, ¿para que querríamos utilizar ConfigureAwait(false)? Pues por ejemplo por motivos de rendimiento. No siempre necesitamos continuar la ejecución en el mismo contexto de sincronización, y en estos casos será mejor si no forzamos a que todo el trabajo se haga en un hilo concreto. Por tanto, ConfigureAwait(false) debería utilizarse siempre que no nos importe dónde debe continuarse la ejecución, que es de hecho en muchas ocasiones, especialmente en código de bajo nivel que realice operaciones con archivos o llamadas a través de la red.

Sin embargo, cuando combinamos código que utiliza contextos de sincronización, ConfigureAwait(false) y llamadas a Result o Wait(), aparece el peligro de deadlocks. En estos casos, la fórmula recomendable para evitarlo es acordarse de utilizar ConfigureAwait(false) en todos los lugares donde explícitamente no sea necesario permanecer en el mismo contexto de sincronización.

Por ejemplo, si vamos a crear una biblioteca propósito general para ser distribuida a través de NuGet, sería muy recomendable introducir ConfigureAwait(false) en todas las llamadas asíncronas, porque no podemos estar seguros del contexto en el que serán utilizadas.

Hay algunas buenas noticias en el horizonte. En ASP.NET Core ya no existe el contexto de sincronización, lo que implica que no necesitaremos utilizar ConfigureAwait(false) con este framework, aunque sigue siendo recomendable cuando creemos paquetes NuGet.

Pero si estáis trabajando en proyectos que corren riesgo de deadlocks, deberéis estar muy atentos y añadir las llamadas a ConfigureAwait(false) en todas partes.

9. Ignorar la versión asíncrona

En todos los métodos de .NET Framework que tardan cierto tiempo o que efectúan E/S en disco o red, casi siempre existe una versión asíncrona que puede ser utilizada en su lugar. Desafortunadamente las versiones síncronas de estos métodos se mantienen por motivos de retrocompatibilidad, pero rara vez encontraremos buenas razones para utilizarlas.

Por ejemplo, es siempre mejor utilizar Task.Delay() que Thread.Sleep(), dbContext.SaveChangesAsync() que dbContext.SaveChanges(), o fileStream.ReadAsync() que fileStream.Read(), pues de esta forma se liberarán hilos del thread pool para poder realizar otras tareas, permitiendo que tus aplicaciones procesen un mayor volumen de solicitudes.

10. Usar try/catch sin await

Existe una optimización muy práctica que probablemente hayáis utilizado alguna vez. Supongamos que tenemos un método asíncrono muy simple, que sólo hace una llamada asíncrona en la última línea del mismo:
public async Task SendUserLoggedInMessage(Guid userId)
{
var userLoggedInMessage = new UserLoggedInMessage() { UserId = userId };
await messageSender.SendAsync("mytopic", userLoggedInMessage);
}
Es esta situación no hay necesidad de utilizar las palabras clave async y await. Podríamos haber hecho simplemente lo siguiente, retornando la tarea directamente:
public Task SendUserLoggedInMessage(Guid userId)
{
var userLoggedInMessage = new UserLoggedInMessage() { UserId = userId };
return messageSender.SendAsync("mytopic", userLoggedInMessage);
}
Tras bambalinas, esto produce un código ligeramente más eficiente, puesto que cuando se utiliza la palabra clave await la compilación genera una máquina de estados.

Pero imaginemos que actualizamos el método de la siguiente manera:
public Task SendUserLoggedInMessage(Guid userId)
{
var userLoggedInMessage = new UserLoggedInMessage() { UserId = userId };
try
{
return messageSender.SendAsync("mytopic", userLoggedInMessage);
}
catch (Exception ex)
{
logger.Error(ex, "Failed to send message");
throw;
}
}
Aunque a primera vista puede parecer correcto, en realidad la cláusula catch no está cumpliendo el cometido que podríamos esperar: no capturará las excepciones que pudieran lanzarse durante la ejecución de la tarea retornada por SendAsync(). Esto se debe a que, en realidad, sólo estamos capturando las excepciones lanzadas mientras se crea la tarea, que es muy distinto.

Si quisiéramos capturar las excepciones lanzadas en cualquier momento durante esa tarea, necesitaríamos de nuevo la palabra clave await:
public async Task SendUserLoggedInMessage(Guid userId)
{
var userLoggedInMessage = new UserLoggedInMessage() { UserId = userId };
try
{
await messageSender.SendAsync("mytopic", userLoggedInMessage);
}
catch (Exception ex)
{
logger.Error(ex, "Failed to send message");
throw;
}
}
Ahora nuestro bloque catch sí podrá capturar las excepciones lanzadas durante la ejecución de la tarea SendAsync().

Conclusión

Hay muchas formas en la que uno puede provocar problemas implementando código asíncrono, por lo que vale la pena dedicar de tiempo a profundizar en la comprensión sobre threading. En este artículo simplemente he citado alguno de los problemas que encuentro con mayor frecuencia, pero estoy seguro de que podrían ser añadidos muchos más. No dudéis en dejar en los comentarios qué consejos adicionales añadiríais a esta lista.

Si os gustaría aprender más sobre threading en C#, algunos recursos que os puedo recomendar son la reciente charla de Bill Wagner en NDC, el curso Getting Started with Asynchronous Programming in .NET de Filip Ekberg y, por supuesto, cualquier cosa escrita por el experto en asincronía Stephen Cleary.

Links:
Publicado en Variable not found.

» Leer más, comentarios, etc...

Fixed Buffer

Terraform: Dando forma a nuestra infraestructura

abril 09, 2019 08:00

La imagen muestra el logo de terraform y el de azure

Acabamos de terminar la serie sobre CI/CD, y en ella hemos hablado de lo importante que es mantener unas estrategias de trabajo que nos permitan la detección de errores rápidamente. Como colofón final, vimos como aprovisionar y desplegar en Azure Web Apps desde el Azure Pipelines (o desde otros sitios) y así despreocuparnos de esa parte utilizando las plantillas ARM.

Utilizar ARM es muy potente, pero quizás es algo tedioso de utilizar… ahí es donde entra terraform. ¿Y que es terraform podrás preguntarte? Pues es una herramienta multiplataforma hecha en Go que nos permite una gran abstracción sobre ARM, pero no solo funciona con ARM, admite una lista de proveedores bastante larga y que puedes consultar aquí.

Además de permitirnos trabajar de forma declarativa, sin tener que bajar al fango, aporta una gran ventaja respecto a usar ARM, y es que terraform mantiene el estado. Es decir, la herramienta es capaz de almacenar el estado de las cosas que ya están desplegadas, evitando volver a desplegarlas si no hay cambios. Pero hablar es fácil, y que yo te cuente que algo es mejor o peor no sirve de mucho, así que vamos a probarlo para ver lo sencillo que es.

Instalando terraform

Vamos a ir a su web y descargarnos el binario para nuestro sistema operativo. En la web nos indican que tenemos que añadir la ruta donde hayamos puesto el binario al PATH. (En mi caso, “C:/Terraform”):

La imagen muestra la ruta de terraform en el PATH

Una vez que lo hayamos hecho, desde una terminal vamos a comprobar que todo esta bien ejecutando:

terraform version

Si todo está bien, debería devolvernos la versión actual. Si es así, ya tenemos terraform instalado. ¿A qué no ha sido tan difícil?

Aunque trabaja con ficheros *.tf que son texto plano, yo recomiendo utilizar Visual Studio Core con la extensión “Terraform“. Si tienes dudas sobre como instalar cualquiera de las dos, te dejo un enlace de CampusMVP donde explico cómo preparar un entorno de trabajo para .Net Core, al que solo hay que añadir la nueva extensión. Esto solo es una sugerencia, si prefieres utilizar cualquier otro entorno, o simplemente el block de notas y la terminal, todo funcionará igual de bien.

Creando nuestro código terraform

Para esta entrada, vamos a hacer un aprovisionamiento sencillo de una Azure Web App Windows, junto a su plan y su grupo de recursos desde terraform (para la siguiente vamos a hacer un escenario algo más complejo añadiendo un Sql Server a nuestra web). Para ello, vamos a crear un fichero al que llamaremos “main.tf” (el nombre no es importante). De hecho, terraform buscará todos los ficheros “*.tf” desde el nivel en el que se encuentre y unificará todos los archivos, por lo que es una buena idea separar los archivos por finalidades. En esta entrada vamos a utilizar un solo fichero para el código, pero una estructura más clara podría ser:

  • webapp.tf para el código de la web.
  • resourcegroup.tf para el código de grupo de recursos.
  • variables.tf para las variables que vamos a necesitar.

Definiendo el proveedor

Lo primero que vamos a hacer, es definir el proveedor junto a las 4 variables que necesita (para utilizar las variables, se hace con el formato “${var.NOMBRE_VARIABLE}” ):

provider "azurerm" {
  version         = ">= 1.6.0"
  subscription_id = "${var.ID_Suscripcion}"
  client_id       = "${var.ID_Aplicacion}"
  client_secret   = "${var.Password_Aplicacion}"
  tenant_id       = "${var.ID_Tenant}"
}
variable "ID_Suscripcion" {
  description = "ID de la suscripción de Azure"
}
variable "ID_Aplicacion" {
  description = "ID del la aplicación"
}
variable "Password_Aplicacion" {
  description = "Secreto de la aplicación"
}
variable "ID_Tenant" {
  description = "ID del directorio activo de Azure"
}

¿Y de dónde sacamos estos datos? Pues dos de ellas simplemente debemos consultarlas, para ello escribimos en la PowerShell:

az account list

Esto nos devolverá el ID de la suscripción y el directorio activo:

La imagen muestra el ID_Suscripcion y el ID_Tenant

Para los dos campos de aplicación, lo que vamos a hacer es registrar una aplicación en nuestro directorio activo. Esto se puede hacer desde el portal de Azure, o desde la propia terminal ejecutando:

az ad sp create-for-rbac -n "NombreAplicacion"

Eso nos va a devolver los datos que nos faltan:

La imagen muestra los datos de ID_Aplicacion y Password_Aplicacion

Si tienes problemas porque no se reconoce el comando “az”, sigue este enlace para instalar Azure CLI.

Definiendo el Grupo de Recursos

Una vez que lo tenemos, vamos a añadir un grupo de recursos para nuestra Web App:

resource "azurerm_resource_group" "webapp" {
  name     = "${var.resource_group_name}"
  location = "${var.location}"
}
variable "location" {
  description = "Region donde queremos que se cree"
}
variable "resource_group_name" {
  description = "Nombre del grupo de recursos"
}

Definiendo el Service Plan

Ahora vamos a añadir el service plan:

resource "azurerm_app_service_plan" "webserviceplan" {
  name                = "${var.service_plan_name}"
  location            = "${azurerm_resource_group.webapp.location}"
  resource_group_name = "${azurerm_resource_group.webapp.name}"

  kind = "${var.plan_settings["kind"]}"

  sku {
    tier     = "${var.plan_settings["tier"]}"
    size     = "${var.plan_settings["size"]}"
    capacity = "${var.plan_settings["capacity"]}"
  }
}
variable "plan_settings" {
  type        = "map"
  description = "Definimos el tipo de plan que vamos a utilizar"

  default = {
    kind     = "Windows"  # Linux or Windows
    size     = "S1"
    capacity = 1
    tier     = "Standard"
  }
}
variable "service_plan_name" {
  description = "Nombre del service plan"
}

Una de las ventajas que podemos ver, es que terraform nos permite utilizar datos de los propios recursos desplegados utilizando el formato “${TIPO_DE_RECURSO.NOMBRE_RECURSO.DATO}”. Por ejemplo, en:

location  = "${azurerm_resource_group.webapp.location}"

Le estamos indicando que la localización del service plan, queremos que sea la misma que la del recurso “azurerm_resource_group” con el nombre “webapp”. Trabajando de esta manera, terraform es capaz de calcular el grafo de dependencias para que todo se pueda aprovisionar sin problemas.

Definiendo la Web App

Por último, solo nos queda añadir el Web App:

resource "azurerm_app_service" "webapp" {
  name                = "${var.name}"
  location            = "${azurerm_resource_group.webapp.location}"
  resource_group_name = "${azurerm_resource_group.webapp.name}"
  app_service_plan_id = "${azurerm_app_service_plan.webserviceplan.id}"
}
variable "name" {
  description = "Nombre de la Web App"
}

Hay muchos parámetros, que podríamos poner en el propio código, pero entonces no sería reutilizable… Terarform nos permite indicarle todos estos parámetros cuando vayamos a hacer el despliegue en el propio comando, pero como puedes intuir, es muy tedioso. Así que vamos a utilizar otra de las opciones que nos da terraform, que es cargar tener un segundo fichero de variables con sus valores.

Si usas git, hay que añadirlo en el .gitignore o nuestras claves se irán al repositorio.

Para poder tener este fichero, creamos un segundo fichero con el formato “*.auto.tfvars”, por ejemplo, “variables.auto.tfvars”, donde iremos rellenando los valores de esta forma:

ID_Suscripcion = "----"
ID_Aplicacion = "----"
Password_Aplicacion = "----"
ID_Tenant = "----"
name = "postterraformbasic"
service_plan_name = "planpostterraformbasic"
resource_group_name = "postterraform"
location = "westeurope"

A la vista está que este código es más fácil de seguir y mantener que si fuese ARM directamente, aunque hay algunas cosas que no están soportadas aun, y puede hacer falta incluir algo de ARM en algunos casos…

Desplegando con terraform

Una vez que tenemos todo el código, y las variables listas, vamos a desplegar, para ello vamos a inicializar la herramienta con:

terraform init
La imagen muestra la salida del comando init

Una vez hecho esto, vamos a comprobar si existe algún error con:

terraform validate

En caso de que no exista ningún error, lo siguiente es calcular los cambios (aunque este paso se puede omitir):

terraform plan

La salida de este comando nos va a mostrar algo como esto:

La imagen muestra la salida del comando terraform plan

Como salida, nos da las acciones que va a ejecutar en detalle, pero solo son a nivel informativo, para ejecutar el despliegue, utilizamos:

terraform apply

Esto nos devuelve algo parecido al comando plan, donde nos dice que es lo que va a hacer, pero ahora, nos pide que confirmemos escribiendo “yes” si queremos que despliegue:

La imagen muestra la petición de confirmación de terraform

Tras escribir “yes”, se inicia el proceso. Una vez termine, la salida será algo asi:

La imagen muestra el resultado de finalización

Como terraform almacena el estado, si volvemos a intentar desplegar lo mismo, volviendo a ejecutar:

terraform apply

Obtenemos el resultado esperado, y nos dice que no hay ningún cambio pendiente:

La imagen muestra como el comando apply termina con 0 cambios

Si por el contrario hubiésemos cambiado algo en el código, el resultado hubiese sido un cambio en nuestra infraestructura. Para terminar con las opciones que tenemos, existe una manera de eliminar los recursos desplegados:

terraform destroy

Volverá a pedirnos confirmación, y basta con que volvamos a escribir “yes”:

La imagen muestra las acciones que va a hacer el comando terraform destroy
La imagen muestra el final del comando terraform destroy

Conclusión

Aunque ha sido un ejemplo simple, se puede ver que terraform es una herramienta muy potente que nos quita mucho trabajo a la hora de desplegar infraestructura desde código al ser declarativo, persistir el estado y encima calcular las dependencias automáticamente. Existe una lista detallada de las opciones disponibles en la documentación oficial, a la cual recomiendo echarle un ojo.

En la próxima entrada vamos a ver un ejemplo más en profundidad creando una web en Linux conectada a un Sql Server, de modo que tengamos una infraestructura funcional, y además desplegaremos la web desde Azure Pipelines para poder comparar el proceso en el caso de ARM y en el caso de Terraform.

Si quieres ver el código completo, dejo un gist. También os dejo el enlace a un repo muy interesante de un workshop sobre terraform de un compañero.

**La entrada Terraform: Dando forma a nuestra infraestructura se publicó primero en Fixed Buffer.**

» Leer más, comentarios, etc...

Variable not found

Enlaces interesantes 357

abril 08, 2019 06:25

Enlaces interesantesAhí van los enlaces recopilados durante la semana pasada, con mucho protagonismo del flamante Visual Studio 2019. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET / .NET Core

ASP.NET / ASP.NET Core

Azure / Cloud

Conceptos / Patrones / Buenas prácticas

Data

Machine learning / IA / Bots

Web / HTML / CSS / Javascript

Visual Studio / Complementos / Herramientas

Otros

Publicado en: www.variablenotfound.com.

» Leer más, comentarios, etc...

Blog Bitix

Trazabilidad en microservicios con Spring Cloud Sleuth

abril 07, 2019 10:45

Spring
Java

En una aplicación distribuida con varios microservicios es imprescindible tener la configuración de forma centralizada que cada microservicio obtiene al iniciarse y disponer de registro y descubrimiento para que los servicios al iniciarse, terminarse, actualizarse o por un fallo se registren o desregistren y obtengan la ubicación de las dependencias que necesitan.

Otra de las funcionalidades esenciales en una aplicación distribuida es la trazabilidad de una petición, desde que entra por el API gateway pasando por las diferentes peticiones que hacen los microservicios por la red o envío de mensajes. Es necesaria la funcionalidad que relacione las trazas de todos los servicios para depuración o consulta en un futuro para dar visibilidad a las acciones que se realizan en el sistema.

¿Como se consigue relacionar las trazas de los microservicios que son independientes? La técnica que se emplea es asignar a cada petición entrante un identificativo, más bien un identificativo para la transacción de forma global y un identificativo para la transacción en cada microservicio que varía en cada comunicación de red.

Cuando un microservicio se comunica con otro envía en su petición el identificativo de la transacción global y el de su transacción. Si un microservicio no recibe estos identificativos los genera. En el protocolo HTTP estos identificativos se envían y reciben a través de las cabeceras. Los identificativos permiten correlacionar todas las trazas que emiten los diferentes procesos de los microservicios de una misma petición en la aplicación, haciendo una búsqueda global por el identificativo global se obtiene el conjunto de trazas que han emitido los microservicios por las que ha transitado una petición.

Para obtener mejor visibilidad de los tiempos y latencias se puede utilizar Zipkin, Prometheus junto con Hystrix o Resilience4j también dan visibilidad de los tiempos entre otras cosas.

En Java el proyecto Spring Cloud Sleuth proporciona la funcionalidad de trazabilidad. En el esquema se observa como Sleuth envía las cabeceras de un servicio cliente a un servicio servidor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 Client Span Server Span
┌──────────────────┐ ┌──────────────────┐
│ │ │ │
│ TraceContext │ Http Request Headers │ TraceContext │
│ ┌──────────────┐ │ ┌───────────────────┐ │ ┌──────────────┐ │
│ │ TraceId │ │ │ X─B3─TraceId │ │ │ TraceId │ │
│ │ │ │ │ │ │ │ │ │
│ │ ParentSpanId │ │ Extract │ X─B3─ParentSpanId │ Inject │ │ ParentSpanId │ │
│ │ ├─┼─────────>│ ├────────┼>│ │ │
│ │ SpanId │ │ │ X─B3─SpanId │ │ │ SpanId │ │
│ │ │ │ │ │ │ │ │ │
│ │ Sampled │ │ │ X─B3─Sampled │ │ │ Sampled │ │
│ └──────────────┘ │ └───────────────────┘ │ └──────────────┘ │
│ │ │ │
└──────────────────┘ └──────────────────┘

Sleuth se encarga de propagar las cabeceras del servicio cliente al servicio servidor automáticamente instrumentando los clientes HTTP de RestTemplate, AsyncRestTemplate, WebClient, Apache HttpClient y Netty HttpClient. Para enviar, recibir, obtener y establecer los identificativos de correlación con Sleuth junto con el cliente HTTP de Java hay que hacer la instrumentación manualmente con las clases Tracing y Tracer si no está entre los soportados como en el caso del cliente HTTP que se añadió en Java 11 en el propio JDK con el soporte para HTTP/2.

En la parte servidora Sleuth proporciona un filtro que se encarga de obtener y crear el span de la petición que contiene los identificativos de correlación que con Spring y las dependencias adecuadas se configura automáticamente. Para inyectar y extraer las cabeceras de Sleuth con el cliente HTTP de Java o como en el ejemplo con el de Jersey basta con proporcionar una lambda que realice el añadido o extracción de las cabeceras con la API del cliente.

Este es el código para instrumentalizar el cliente HTTP de Jersey que utiliza el servicio cliente que invoca al gateway y el código para crear el span en el cliente con los identificativos de correlación y recogerlos en el servicio servidor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package io.github.picodotdev.blogbitix.springcloud.client;
...
@Component
public class ProxyService {
@Autowired
private LoadBalancerClient loadBalancer;
@Autowired
private Tracing tracing;
@Autowired
private Tracer tracer;
@HystrixCommand(fallbackMethod = "getFallback", commandProperties = {
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "4"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "25000")
})
public String get() {
ServiceInstance instance = loadBalancer.choose("proxy");
URI uri = instance.getUri();
String resource = String.format("%s%s", uri.toString(), "/service");
Invocation.Builder builder = ClientBuilder.newClient().target(resource).request();
Span span = tracer.newTrace().start();
TraceContext.Injector<Invocation.Builder> injector = tracing.propagation().injector((Invocation.Builder carrier, String key, String value) -> { carrier.header(key, value); });
injector.inject(span.context(), builder);
System.out.printf("Proxy Span (traceId: %s, spanId: %s)%n", span.context().traceIdString(), span.context().spanIdString());
return builder.get().readEntity(String.class);
}
private String getFallback() {
return "Fallback";
}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package io.github.picodotdev.blogbitix.springcloud.service;
...
@RestController
public class DefaultController {
@Autowired
private DefaultConfiguration configuration;
@Autowired
private Tracing tracing;
@Autowired
private Tracer tracer;
private Random random;
private Counter counter;
public DefaultController(MeterRegistry registry) {
this.random = new Random();
this.counter = Counter.builder("service.invocations").description("Total service invocations").register(registry);
}
@RequestMapping("/")
public String home(HttpServletRequest request) throws Exception {
counter.increment();
// Timeout simulation
 //Thread.sleep(random.nextInt(2000));

TraceContext.Extractor<HttpServletRequest> extractor = tracing.propagation().extractor((HttpServletRequest carrier, String key) -> { return carrier.getHeader(key); });
Span span = tracer.nextSpan(extractor.extract(request));
System.out.printf("Client Span (traceId: %s, spanId: %s)%n", span.context().traceIdString(), span.context().spanIdString());
return String.format("Hello world (%s, %s)", request.getRequestURL(), configuration.getKey());
}
}

He utilizado el ejemplo de la serie de artículos sobre Spring Cloud añadiendo el soporte para Spring Cloud Sleuth. La aplicación se compone de un microservicio de configuración (con Spring Cloud Config), otro de registro y descubrimiento (con Eureka), un servicio de API gateway (con Zuul), el servicio de aplicación y un cliente del servicio que envía las peticiones al gateway y este las redirige al servicio de aplicación.

El cliente inicia un span que es enviado al servidor y el servidor obtiene las cabeceras enviadas. El cliente y el servidor son dos procesos distintos del sistema pero se observa que el identificativo global de la transacción traceId se mantiene en ambos y el identificativo de spanId cambia entre el cliente y el servidor.

1
2
3
4
5
Client Span (traceId: 94fcd131178298fd, spanId: 94fcd131178298fd)
...
Service Span (traceId: 94fcd131178298fd, spanId: 6e6380e239f30917)
...
Service response: Hello world (http://archlinux:8080/, value)

Para iniciar los diferentes microservicios de la aplicación hay que utilizar los siguientes comandos.

1
2
3
4
5
$ ./gradlew discoveryserver:run --args="--port=8761"
$ ./gradlew configserver:run --args="--port=8090"
$ ./gradlew service:run --args="--port=8080"
$ ./gradlew proxy:run --args="--port=8085"
$ ./gradlew client:run --args="--service=proxy"

En los proyectos hay que incluir la dependencia para Sleuth en la herramienta de construcción.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
plugins {
id 'application'
}
mainClassName = 'io.github.picodotdev.blogbitix.springcloud.client.Main'
dependencies {
implementation platform("org.springframework.boot:spring-boot-dependencies:2.1.4.RELEASE")
implementation platform("org.springframework.cloud:spring-cloud-dependencies:Greenwich.SR1")
// Spring
 def excludeSpringBootStarterLogging = { exclude(group: 'org.springframework.boot', module: 'spring-boot-starter-logging') }
compile('org.springframework.boot:spring-boot-starter', excludeSpringBootStarterLogging)
...
compile('org.springframework.cloud:spring-cloud-starter-sleuth', excludeSpringBootStarterLogging)
...
}

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradle-run.sh.

» Leer más, comentarios, etc...

Blog Bitix

El concepto de wildcard capture en Java

abril 05, 2019 03:45

Java

Con la introducción de los generics en el lenguaje Java en la versión de Java 5 se añadió validación de tipos a por ejemplo las colecciones, y entre ellos los elementos wildcard definidos con un ?. Una lista definida como List<?> se considera una lista de elementos de un tipo desconocido, todas las colecciones pre-java5 se consideran a partir de Java 5 de forma efectiva como List<?> o List<? extends Object> a partir de Java 5.

El siguiente código produce un error de compilación con el mensaje capture of ya que el compilador no puede validar que el tipo que se inserta en la lista, Object, como primer elemento si es compatible en tiempo de ejecución con el tipo de elementos que tiene la lista:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.ArrayList;
import java.util.List;
public class WildcardError {
public void foo(List<?> list) {
Object object = list.get(0);
list.set(0, object);
}
// @SuppressWarnings("unchecked")
 public void bar(List list) {
Object object = list.get(0);
list.set(0, object);
}
public static void main(String[] args) {
List<String> list = new ArrayList<>();
WildcardError wildcard = new WildcardError();
wildcard.foo(list);
wildcard.bar(list);
}
}
1
2
3
4
5
6
7
8
9
WildcardError.java:8: error: incompatible types: Object cannot be converted to CAP#1
list.set(0, object);
^
where CAP#1 is a fresh type-variable:
CAP#1 extends Object from capture of ?
Note: WildcardError.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error

El método bar() define como parámetro una lista raw y es capaz de extraer un Object ya que todo objeto hereda de él e insetar un Object ya que es una lista raw, el compilador realiza el type erasure y la considera como List<Object> pero el compilador advierte del posible error en tiempo de ejecución con el mensaje Note: WildcardError.java uses unchecked or unsafe operations, en este caso la advertencia es innecesaria ya que se inserta un elemento extraído de la propia lista, se puede suprimir anotando el método con @SuppressWarnings(“unchecked”).

Para establecer una relación entre dos tipos se deben usar type parameters, en este caso para el tipo que se extrae de la lista y el tipo insertado en la lista. Para que el código anterior compile hay que escribir un método que capture el tipo del wildcard, estos métodos por convención se nombran añadiendo al final la palabra Helper. En este caso otra alternativa es definir el método como en bar() aunque un List<?> y un List<T> no son lo mismo el primero admite más tipos de listas parametrizadas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.ArrayList;
import java.util.List;
public class Wildcard {
public void foo(List<?> list) {
fooHelper(list);
}
private <T> void fooHelper(List<T> list) {
T object = list.get(0);
list.set(0, object);
}
public <T> void bar(List<T> list) {
T object = list.get(0);
list.set(0, object);
}
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Wildcard wildcard = new Wildcard();
wildcard.foo(list);
wildcard.bar(list);
}
}

Este concepto de wildcard capture genera bastantes dudas y en internet hay múltiples artículos tratando de explicarlo.

» Leer más, comentarios, etc...

Picando Código

Descarga gratis el número de abril de la revista Linux Journal

abril 03, 2019 07:08

La revista Linux Journal cumple 25 años. Para celebrarlo, pusieron para descargar gratis en su sitio web. Simplemente hay que visitar esta página, ingresar una dirección de correo electrónico, y recibir un enlace de descarga. Les recomiendo inscribirse al newsletter de la revista, semanalmente envían artículos de interés a los usuarios de GNU/Linux y el software libre en general.

Este número en particular viene bastante cargado, entre otras cosas dos entrevistas a Linus Torvalds por el primer editor de la revista, Robert Young (quien luego fundara Red Hat). La primera una entrevista actual donde Linus habla de varias cosas. Interesante saber que cuando necesita portabilidad, usa una Dell XPS 13, el mismo modelo que uso en el trabajo. La segunda entrevista es la republicación de una entrevista publicada originalmente en el primer número de Linux Journal en 1994, con los mismos protagonistas.

Además hay artículos sobre el futuro de Linux, Software Libre y los niños, Kubernetes, cómo armar tu propio radio receptor, y más.
Pueden pedir el enlace de descarga en: https://www.linuxjournal.com/free_issue

Linux Journal 25

 

 

» Leer más, comentarios, etc...

Picando Código

Se viene ScaLATAM: la primera conferencia focalizada en Scala y Programacion Funcional de Latinoamérica

abril 03, 2019 02:00

Scalatam

En mayo se organiza en Montevideo ScaLATAM: la primera conferencia focalizada en Scala y Programacion Funcional de Latinoamérica. Entre los oradores está Gabriel Claramunt, referente de la comunidad Scala y el culpable de que me empezara a interesar la programación funcional allá cuando éramos jóvenes y me apasionaba la programación. ¡Por más que no usen Scala les recomiendo asistir! Qué buenas conferencias está habiendo en Uruguay…
Más información:

El objetivo es reunir a developers, empresas y entusiastas en un ambiente pensado para el intercambio de conocimientos y aprendizaje. La conferencia consiste en dos días de charlas dadas por speakers reconocidos local e internacionalmente con experiencia y conocimiento de las nuevas tendencias. Elegimos Uruguay como sede ya que es uno de los principales exportadores de software y donde las empresas mas importantes de la región deciden instalarse. Si estás dando tus primeros pasos en Scala o si ya tenés experiencia, querés potenciar tus conocimientos y contactarte con empresas/personas que comparten tus intereses, esta conferencia es para vos!

Llamado a charlas

Está abierto el llamado a charlas para que envíes tu propuesta. Temas priorizados (pero no excluyentes, si tenés una idea, ¡enviala!): FP Foundations, FP Experience reports (Industrial adoption, Open source , Growing the Community),  Scala Libraries and applications (Distributed systems, ML, Big Data), Development Workflow, Scala Tooling. Se puede en este enlace.

Entradas

Hay entradas generales a USD 30 y para estudiantes a USD 15 y se pueden comprar acá.

Dónde y cuándo

Jueves 2 y Viernes 3 de Mayo
Universidad ORT Uruguay, Cuareim 1451, Montevideo, Uruguay

» Leer más, comentarios, etc...

Variable not found

¿Se pueden asociar varias interfaces a la misma instancia en el contenedor de servicios de ASP.NET Core?

abril 02, 2019 06:57

ASP.NET Core Hace unas semanas, el amigo J. Roldán escribía un comentario en el post Inyección de dependencias en ASP.NET Core con una pregunta interesante sobre la forma de registrar determinados componentes en el inyector de dependencias. Básicamente la duda que planteaba era cómo asociar distintas interfaces a una única instancia, algo que, aunque no es complicado de conseguir, tampoco tiene una solución precisamente intuitiva.

El problema

En el escenario concreto que planteaba nuestro querido lector era que quería proporcionar, a los componentes que necesitaban acceder a datos en su aplicación, un acceso limitado al contexto de datos de Entity Framework.

Para ello, por un lado definía una interfaz parecida a la siguiente, que proporcionaba acceso a los repositorios:
public interface IUserRepositories
{
DbSet<User> Users { get; }
...
}
Por otra parte, definía otra interfaz que era la que permitía comprometer cambios (adiciones, supresiones, modificaciones) realizadas en el contexto de datos:
public interface IUnitOfWork
{
int SaveChanges();
}
La idea de esta separación es que si un componente necesitaba exclusivamente consultar datos relativos a usuarios, sólo recibiría mediante inyección de dependencias la instancia de IUserRepositories, mientras que si necesitaba persistir datos, recibiría adicionalmente un objeto IUnitOfWork, por ejemplo:
public class UserServices
{
...
public UserServices(IMapper mapper, IUserRepositories userRepos, IUnitOfWork uow)
{
_mapper = mapper;
_userRepos = userRepos;
_uow = uow;
}

public async Task Update(int id, UserDto user)
{
var user = await _userRepos.Users.FirstOrDefaultAsync(u=>u.Id == id);
if(user == null)
throw new Exception();

_mapper.Map(userDto, user);
await _uof.SaveChangesAsync();
}
}
Bien, pues el problema lo tenía precisamente a la hora de registrar las dependencias de forma apropiada.

Si, como es lo habitual, registraba ambos componentes como scoped, las instancias del contexto de datos que recibía eran distintas, por lo que los cambios realizados en una no eran visibles para la otra, aparte de la sobrecarga que supone crear dos contextos en cada petición:
// En ConfigureServices():

services.AddScoped<IRepositories, EfContext>();
services.AddScoped<IUnitOfWork, EfContext>();

// En UserServices:
public class UserServices
{
...
public UserServices(IUserRepositories userRepos, IUnitOfWork uow, IMapper mapper)
{
if(userRepos != uow) // userRepos contiene una instancia del contexto
throw new Exception("Ooops!"); // uow contiene otra instancia distinta del contexto
_mapper = mapper;
_userRepos = userRepos;
_uow = uow;
}
...
}

La solución

Obviamente, lo que hay que cambiar es la forma de registrar las interfaces, de forma que ambas estén asociadas a la misma instancia del contexto de datos, única en el ámbito de una petición.

Para ello, podemos hacer lo que se muestra en el siguiente código:
services.AddScoped<IUserRepositories, EfContext>();
services.AddTransient<IUnitOfWork>(provider => (IUnitOfWork)provider.GetService<IUserRepositories>());
Como podemos observar, en primer lugar asociamos la interfaz IUserRepositories a una instancia per request del contexto de datos EfContext. Hasta ahí, lo normal.

La diferencia viene ahora: la interfaz IUnitOfWork la registramos utilizando la sobrecarga que permite especificar la factoría que será utilizada para obtener la instancia, y desde ella retornamos el objeto que ya esté asociado a IUserRepositories.

Sencillo, ¿verdad?

Publicado en: www.variablenotfound.com.

» Leer más, comentarios, etc...

Koalite

Tipos Suma en TypeScript

abril 01, 2019 05:06

A veces da la sensación de que TypeScript se usa “sólo” como una forma de tener Javascript con intellisense y errores de compilación. La verdad es que, sólo por eso, ya puede merecer la pena, pero si nos quedamos ahí nos estamos perdiendo parte de las ventajas que nos puede aportar a la hora de diseñar soluciones.

Quizá el sistema de tipos de TypeScript no sea el mejor del mundo y presente sus problemas a la hora de mantener invariantes o no tenga toda la solidez que a uno le pudiera gustar, pero eso no es óbice para que ofrezca alternativas de diseño interesantes, sobre todo comparado con las que solemos ver en lenguajes como C# o Java.

En este post vamos a ver cómo podemos aprovechar varias características del sistema de tipos, concretamente los tipos literales y los tipos unión, para conseguir un diseño más seguro.

Tipos literales

Se le llama tipo literal en TypeScript a aquel tipo que contiene un único valor de tipo string, number, boolean o enum. Por ejemplo:

type Two = 2;
type Duck = 'Duck';

const x: Two = 2; // OK
const y: Two = 3; // Error

const a: Duck = 'Duck'; // OK
const b: Duck = 'Fox'; // Error

A primera vista no parece algo muy útil porque todas las variables que definamos del tipo tendrán el mismo valor, pero cuando los combinamos con otros tipos la cosa mejora.

Las reglas de inferencia de tipos para los tipos literales son un poco confusas (y han cambiado alguna vez a lo largo de la vida de TypeScript), por lo que hay que andarse con un poco de ojo:

let x = 2; // el tipo inferido para x es "Number"
const x = 2; // el tipo inferido para x es 2

Por supuesto, un valor de un tipo literal es asignable a valores del tipo “general”, pero no a la inversa:

const x: Two = 2;
const y: Number = x; // OK
const z: Two = y; // Error

Tipos unión

En TypeScript podemos definir un tipo formado por la unión de otros tipos, es decir, a una variable del tipo unión podemos asignarle cualquier valor de cualquiera de los tipos que forman la unión:

type StringOrNumber = String | Number;

let a: StringOrNumber = 2; // OK
let b: StringOrNumber = 'Mar'; // OK
let c: StringOrNumber = null; // Error

Como no, nuestros tipos literales pueden formar parte de uniones:

type DuckOrTwo = 'Duck' | 2;

let a: DuckOrTwo = 2; // OK
let b: DuckOrTwo = 'Duck'; // OK
let c: DuckOrTwo = 142.12; // Error

Esto facilita interoperar con apis Javascript donde es frecuente que algún parámetro pueda tomar un conjunto de valores primitivos. Un caso típico sería el método addEventListener para registrar manejadores de eventos, al cual se le pasa el nombre del evento para el que se quiere registrar el manejador.

Tipos suma

Juntando los tipos literales y los tipos unión podemos construir el equivalente a tipos suma. Estos tipos pueden almacenar información de distintos tipos, y existe una forma de detectar cuál de los posibles tipos almacenan.

Dicho así suena un poco enrevesado, pero es fácil entenderlo con un poco de código. Imagina que necesitas representar un valor opcional. Para ello hace falta indicar de alguna forma la ausencia de valor, algo que muchas veces se hace utilizando null o undefined, pero esa alternativa no siempre es aconsejable. Podemos mejorarlo haciendo algo así:

type Optional<T> = { empty: true } | { empty: false, value: T };

function f(x: Optional<number>) {
  if (x.empty === true) {
    // Tipo inferido { empty: true }
    console.log(x.value); // error de compilación
  }
  else {
    // Tipo inferido { empty: false, value: number }
    console.log(x.value * 2);
  }
}

Es un poco feo tener que escribir el x.empty === true, pero sin eso el sistema de tipos de TypeScript no es capaz de acotar el tipo correcto en cada rama del if.

Todo esto podemos aplicarlo a cosas algo más interesantes que Optional. Veamos otro ejemplo en el que tenemos usuarios con una dirección de correo asociado y queremos que no se les pueda enviar emails hasta que la dirección de correo haya sido verificada.

Modelar esto en C# con una cierta seguridad de tipos es relativamente incómodo y nos obliga a utilizar varias clases y alguna relación de herencia por el camino. En TypeScript es bastante sencillo:

type VerifiedEmail = { verified: true, address: string };
type UnverifiedEmail = { verified: false, address: string };
type Email = VerifiedEmail | UnverifiedEmail;

type User = { name: string, email: Email };

function sendEmail({ address }: VerifiedEmail) {
    // TODO: Enviar email
}

function verifyEmail({ address }: UnverifiedEmail) {
    // TODO: Verificar el email
}

let u: User = getUserFromSomewhere();

sendEmail(u.email); // Error. Email podría no estar verificado

if (u.email.verified === true)
    sendEmail(u.email);
else
    verifyEmail(u.email);

De esta forma es fácil crear tipos más restrictivos que nos permitan limitar las operaciones disponibles en cada caso y nos obliguen a tener en cuenta todos los escenarios posibles.

En realidad, como decíamos antes, lo único que estamos haciendo es implementar tipos suma de una forma un tanto rebuscada, usando un valor dentro del tipo como discriminador en lugar de usar constructores como haríamos, por ejemplo, en Haskell donde nuestro tipo Email se definiría así:

data Email = VerifiedEmail address | UnverifiedEmail address

En lugar de utilizar un flag para distinguir entre los dos estados de Email se utilizan distintos constructores, VerifiedEmail y UnverifiedEmail para diferenciar un tipo de correo de otro.

Conclusión

TypeScript no es la panacea y tiene un montón de cosas discutibles (soy el primero que se suele quejar de ello), pero también ofrece posibilidades interesantes.

Desgraciadamente, en el mundo del frontend es muy frecuente “no diseñar” y limitarse a aplicar los patrones indicados por el framework o librería de turno (react, vue, redux, angular…). Eso hace que TypeScript se vea sólo como una forma de tipar estáticamente las llamadas a estas herramientas y no se intente aprovechar realmente la capacidad de diseño que permite su sistema de tipos.

Como vimos al hablar de las clases como code smell, es importante utilizar cada lenguaje sacando partido a sus características en lugar de limitarse a traducir de un lenguaje a otro. Si tu uso de TypeScript se reduce a emplear las mismas técnicas que en Javascript (pero con intellisense) o los mismos diseños que en C# (pero ejecutándose en un browser), te estás perdiendo gran parte de sus ventajas.

Posts relacionados:

  1. Extender tipos existentes en TypeScript
  2. TypeScript: varianza y solidez
  3. Mantenimiento de invariantes en TypeScript

» Leer más, comentarios, etc...

Blog Bitix

Ejemplo de máquina de estados con Spring Statemachine

marzo 31, 2019 09:00

Spring
Java

Hace ya unos años escribí un ejemplo y un artículo con una implementación propia de una máquina de estado en Java, para algún caso muy básico puede ser suficiente pero para algo serio no es la opción a elegir. Pasado un tiempo de ese ejemplo y artículo descubrí uno de los muchos proyectos de Spring útiles para una aplicación, para las necesidades más habítuales tiene un proyecto que lo proporciona y para las menos habituales es también posible que la proporcione como en el caso del proyecto Spring Statemachine que precisamente tiene el mismo objetivo de implementar una máquina de estados cubriendo muchos casos de uso.

El uso de una máquina de estados permite modelar un proceso con cierta complejidad. Puede ser el ciclo de vida de una compra, un envío, un proceso documental, financiero, … cualquiera en el que intervengan estados, transiciones entre esos estados y realización de acciones necesarias para proporcionar la funcionalidad deseada.

Una máquina de estados se compone de un conjunto de estados finito, de transiciones en esos estados, de eventos que disparan las transiciones y cambios de estado, de acciones asociadas a los estados, a las transiciones o a la entrada o salida de un estado, guards que permiten decidir que transición se escoge entre varias en los choices, forks en las que el flujo sigue por varios caminos en paralelo, temporizadores que pasado un tiempo disparan una transición, seguridad para proteger la ejecución de eventos, transiciones y acciones con Spring Security, persistencia tanto para la configuración como para el estado en bases de datos relacionales o NoSQL como Redis y MongoDB. Una lista bastante completa de características que cubrirá las necesidades de la mayoría de aplicaciones.

Aparte de proporcionar una librería para crear máquinas de estados en Java otra ventaja es que el fujo de un proceso queda recogido y documentado en la implementación de la máquina de estados. De que estados, transiciones y eventos se compone en un punto más centralizado lo que en otro caso podría estar repartido por el código de la aplicación utilizando una solución propia codificada expresamente para el caso.

En el ejemplo he definido el siguente grafo de estados y transiciones que contiene estado inicial, choice, fork, join, jerarquía de estados con en el estado Tasks y un estado final. Una selección completa del conjunto de tipos de estados. En este caso las flechas van únicamente en una dirección pero perfectamente el flujo podría tener transiciones que volviesen a estados anteriores creando ciclos.

Grafo de la máquina de estados

El uso de Spring Statemachine es relativamente sencillo. En el ejemplo se define en un enumerado la lista de estados, también la lista de eventos o transiciones. Una de las formas de definir el grafo dela máquina de estados es mediante código Java y utilizando Spring definiendo un bean. La clase StateMachineBuilder facilita la construcción de la definición de la máquina de estados, por una parte está la configuración general o de infraestructura con el método configureConfiguration(). Con el método configureStates() se define los estados de la máquina y su tipo (inicial, final, normal, choice, fork, join,) así como las acciones que se deseen ejecutar al entrar, en el estado y al salir del estado con los métodos stateEntry(), stateDo() y stateExit(). Con el método withStates()() se pueden definir submáquinas o una jerarquía de estados como en el caso del estado Tasks. Finalmente hay que definir cuales son las posibles transiciones de los estados y que eventos las disparan. Normalmente las transiciones se definen con el método withExternal() pero en los casos de los estados de tipo choice, fork y join se define con los métodos withChoice(), withFork() y withJoin().

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package io.github.picodotdev.blogbitix.springstatemachine;
...
@SpringBootApplication
public class Main implements CommandLineRunner {
public static enum States {
START, STATE1, CHOICE, CHOICE1, CHOICE2, FORK, TASKS, TASK11, TASK12, TASK13, TASK21, TASK22, TASK23, JOIN, STATE2, END
}
public static enum Events {
START_STATE1, STATE1_CHOICE, CHOICE1_FORK, CHOICE2_FORK, TASK11_TASK12, TASK12_TASK13, TASK21_TASK22, TASK22_TASK23, STATE2_END
}
@Autowired
private ApplicationContext context;
@Autowired
private StateMachine<States, Events> machine1;
@Bean
public StateMachine<States, Events> buildMachine(DefaultAction action, DefaultErrorAction errorAction, DefaultStateMachineEventListener listener) throws Exception {
StateMachineBuilder.Builder<States, Events> builder = StateMachineBuilder.builder();
// https://github.com/spring-projects/spring-statemachine/issues/354
 ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setDaemon(true);
taskScheduler.initialize();
builder.configureConfiguration().withConfiguration()
.taskScheduler(taskScheduler);
builder.configureStates()
.withStates()
.initial(States.START)
.stateEntry(States.STATE1, action, errorAction)
.stateDo(States.STATE1, action, errorAction)
.stateExit(States.STATE1, action, errorAction)
.choice(States.CHOICE)
.state(States.CHOICE1)
.state(States.CHOICE2)
.fork(States.FORK)
.state(States.TASKS)
.join(States.JOIN)
.state(States.STATE2)
.end(States.END)
.and()
.withStates()
.parent(States.TASKS)
.initial(States.TASK11)
.state(States.TASK12)
.end(States.TASK13)
.and()
.withStates()
.parent(States.TASKS)
.initial(States.TASK21)
.state(States.TASK22)
.end(States.TASK23);
builder.configureTransitions()
.withExternal()
.source(States.START).target(States.STATE1)
.event(Events.START_STATE1)
.action(Actions.errorCallingAction(action, errorAction))
.and()
.withExternal()
.source(States.STATE1).target(States.CHOICE)
.event(Events.STATE1_CHOICE)
.and()
.withChoice()
.source(States.CHOICE)
.first(States.CHOICE1, new RandomGuard())
.last(States.CHOICE2)
.and()
.withExternal()
.source(States.CHOICE1)
.target(States.FORK)
.event(Events.CHOICE1_FORK)
.and()
.withExternal()
.source(States.CHOICE2)
.target(States.FORK)
.event(Events.CHOICE2_FORK)
.and()
.withFork()
.source(States.FORK)
.target(States.TASKS)
.and()
.withExternal()
.state(States.TASKS)
.source(States.TASK11)
.target(States.TASK12)
.event(Events.TASK11_TASK12)
.and()
.withExternal()
.source(States.TASK12)
.target(States.TASK13)
.event(Events.TASK12_TASK13)
.and()
.withExternal()
.source(States.TASK21)
.target(States.TASK22)
.event(Events.TASK21_TASK22)
.and()
.withExternal()
.source(States.TASK22)
.target(States.TASK23)
.event(Events.TASK22_TASK23)
.and()
.withJoin()
.source(States.TASK13)
.source(States.TASK23)
.target(States.JOIN)
.and()
.withExternal()
.source(States.JOIN)
.target(States.STATE2)
.and()
.withExternal()
.source(States.STATE2)
.target(States.END)
.event(Events.STATE2_END);
StateMachine<States, Events> stateMachine = builder.build();
stateMachine.addStateListener(listener);
return stateMachine;
}
...
public static void main(String... args) {
SpringApplication.run(Main.class, args);
}
}

Las acciones asociadas a los estados del ejemplo simplemente emiten trazas pero tienen disponible el parámetro context para obtener datos e implementar su lógica, como es un componente de Spring podría incluso hacer uso de inyección de dependencias para utilizar otros servicios que necesitase.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package io.github.picodotdev.blogbitix.springstatemachine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.action.Action;
import org.springframework.stereotype.Component;
@Component
public class DefaultAction implements Action<Main.States, Main.Events> {
private static Logger logger = LoggerFactory.getLogger(DefaultAction.class);
@Override
public void execute(StateContext<Main.States, Main.Events> context) {
logger.info("Action Source: {}, State: {}, Target: {}, Event: {}", (context.getSource() != null) ? context.getSource().getId() : null, context.getStateMachine().getState().getId(), (context.getTarget() != null) ? context.getTarget().getId() : null, context.getEvent());
}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package io.github.picodotdev.blogbitix.springstatemachine;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.action.Action;
import org.springframework.stereotype.Component;
@Component
public class DefaultErrorAction implements Action<Main.States, Main.Events> {
@Override
public void execute(StateContext<Main.States, Main.Events> context) {
context.getException().printStackTrace();
}
}

En el caso de un choice se utilizan los métodos first(), then() y last() que una cláusula guard en los dos primeros determinan a que estado se transiciona. Una clásula guard es una instancia de la clase Guard que contiene un método que devuelve un boolean según su lógica. Si es verdadero se seleciona el estado asociado, para la lógical se puede utilizar la información de contexto StateContext entre otra información como la que se haya asociado en los diferentes estados con el método getExtendedState() y la proporcionada en la ejecución del evento con getMessageHeaders().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package io.github.picodotdev.blogbitix.springstatemachine;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.guard.Guard;
import java.util.Random;
public class RandomGuard implements Guard<Main.States, Main.Events> {
@Override
public boolean evaluate(StateContext<Main.States, Main.Events> context) {
return new Random().nextBoolean();
}
}

En este ejemplo se utiliza Spring Boot, aparte de las configuraciones anteriores se realizan dos cosas adicionales. Una de ellas es utilizar un ThreadPoolTaskScheduler en modo demonio de modo que la máquina virtual y el programa pueda finalizar correctamente. La otra es que con una implementación de la clase StateMachineListenerAdapter se pueden recibir que acciones realiza la máquina de estados, en este caso para emitir trazas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package io.github.picodotdev.blogbitix.springstatemachine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.Message;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.listener.StateMachineListenerAdapter;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.transition.Transition;
import org.springframework.stereotype.Component;
@Component
public class DefaultStateMachineEventListener extends StateMachineListenerAdapter<Main.States, Main.Events> {
private static Logger logger = LoggerFactory.getLogger(DefaultStateMachineEventListener.class);
@Override
public void stateChanged(State<Main.States, Main.Events> from, State<Main.States, Main.Events> to) {
logger.info("Listener stateChanged from {} to {}", (from != null) ? from.getId() : null, (to != null) ? to.getId() : null);
}
@Override
public void stateEntered(State<Main.States, Main.Events> state) {
logger.info("Listener stateEntered in {}", state.getId());
}
@Override
public void stateExited(State<Main.States, Main.Events> state) {
logger.info("Listener stateExited in {}", state.getId());
}
@Override
public void transition(Transition<Main.States, Main.Events> transition) {
logger.info("Listener transition from {} to {}", (transition.getSource() != null) ? transition.getSource().getId() :null, (transition.getTarget() != null) ? transition.getTarget().getId() : null);
}
@Override
public void transitionStarted(Transition<Main.States, Main.Events> transition) {
logger.info("Listener transitionStarted from {} to {}", (transition.getSource() != null) ? transition.getSource().getId() : null, (transition.getTarget() != null) ? transition.getTarget().getId() : null);
}
@Override
public void transitionEnded(Transition<Main.States, Main.Events> transition) {
logger.info("Listener transitionEnded from {} to {}", (transition.getSource() != null) ? transition.getSource().getId() : null, (transition.getTarget() != null) ? transition.getTarget().getId() : null);
}
@Override
public void stateMachineStarted(StateMachine<Main.States, Main.Events> stateMachine) {
logger.info("Listener stateMachineStarted");
}
@Override
public void stateMachineStopped(StateMachine<Main.States, Main.Events> stateMachine) {
logger.info("Listener stateMachineStopped");
}
@Override
public void eventNotAccepted(Message<Main.Events> event) {
logger.info("Listener eventNotAccepted {}", event);
}
@Override
public void extendedStateChanged(Object key, Object value) {
logger.info("Listener extendedStateChanged {} to {}", key, value);
}
@Override
public void stateMachineError(StateMachine<Main.States, Main.Events> stateMachine, Exception exception) {
logger.info("Listener stateMachineError {}", exception.getMessage());
}
@Override
public void stateContext(StateContext<Main.States, Main.Events> stateContext) {
logger.info("Listener stateContext (Message Payload: {}, Message Headers: {}, Variables: {}", (stateContext.getMessage() != null) ? stateContext.getMessage().getPayload() : null, stateContext.getMessageHeaders(), stateContext.getExtendedState().getVariables());
}
}

Una vez definida la máquina de estados su uso comienza con el método start(), a continuación ya se pueden enviar eventos par provocar cambios de estado y ejecución de acciones. Se puede proporcionar información de contexto para la máquina accesible desde cualquier estado en el mapa obtenido con ExtendedState.getVariables() y al provocar eventos con la clase GenericMessage. En cualquier momento el método getState() devuelve en que estado se encuentra la máquina. Según la rama que haya elegido la clase RandomGuard en la bifurcación y el estado en el que esté la máquina se envía la transición CHOICE1_FORK o CHOICE2_FORK. El fork provoca que la máquina de estados ejecute en paralelo dos caminos, enviados los eventos correctos se llega a los estados finales de las dos ramas del subestado y se pasa automáticamente al estado JOIN que a su vez pasa automáticamente a STATE2, finalmente se pasa al estado END y termina la máquina de estados.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package io.github.picodotdev.blogbitix.springstatemachine;
...
@SpringBootApplication
public class Main implements CommandLineRunner {
...
@Override
public void run(String... args) throws Exception {
Random random = new Random();
machine1.start();
machine1.getExtendedState().getVariables().put("variable", 42);
machine1.sendEvent(new GenericMessage<>(Events.START_STATE1, Collections.singletonMap("key", 31)));
machine1.sendEvent(Events.STATE1_CHOICE);
machine1.sendEvent((machine1.getState().getId() == States.CHOICE1) ? Events.CHOICE1_FORK : Events.CHOICE2_FORK);
machine1.sendEvent(Events.TASK11_TASK12);
machine1.sendEvent(Events.TASK21_TASK22);
machine1.sendEvent(Events.TASK12_TASK13);
machine1.sendEvent(Events.TASK22_TASK23);
machine1.sendEvent(Events.STATE2_END);
machine1.stop();
...
}
public static void main(String... args) {
SpringApplication.run(Main.class, args);
}
}

Otro caso de uso posible es en vez de iniciar la máquina de estados desde el inicio es iniciarla en un determinado estado para continuar con el flujo de una entidad que anteriormente se quedó en ese estado. En el caso del ejemplo la máquina de estados se continua desde el estado TASK11 y TASK22 utilizando el método resetStateMachine(), se vuelve a iniciar la máquina y se envían los eventos adecuados a partir de esos estados.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package io.github.picodotdev.blogbitix.springstatemachine;
...
@SpringBootApplication
public class Main implements CommandLineRunner {
...
@Override
public void run(String... args) throws Exception {
...
machine1.start();
machine1.sendEvent(Events.TASK11_TASK12);
machine1.sendEvent(Events.TASK22_TASK23);
machine1.sendEvent(Events.TASK12_TASK13);
machine1.sendEvent(Events.STATE2_END);
machine1.stop();
}
public static void main(String... args) {
SpringApplication.run(Main.class, args);
}
}

Por defecto el contenedor de dependencias de Spring utiliza el @Scope singleton para los beans de modo que solo existe una única instancia, como las máquinas tiene estado no pueden compartirse, hay que crear una nueva en caso de querer utilizar dos simultáneamente como sería el caso de una aplicación web o un proceso que escucha mensajes de una cola, utilizando el scope prototype se crea una instancia cada vez que se necesite. La creación de más máquina de estado se indica en la documentación que es algo costoso para no tener que crearlas, tener varias instancias y limitado a cierto número se puede utilizar un pool de máquinas de estado.

En un cambio de estado se produce la siguiente secuencia de eventos y acciones.

  • Se notifica del inicio de la transición.
  • Se ejecuta la acción asociada a la transición.
  • Se notifica de la transición, de la salida del estado anterior y de la entrada en el nuevo.
  • Se ejecuta la acción de entrada al estado y del estado.
  • Se notifica del cambio de estado.
  • Se notifica de la finalización de la transición.
1
2
3
4
5
6
7
8
9
... : Listener transitionStarted from START to STATE1
... : Action Source: START, State: START, Target: STATE1, Event: START_STATE1
... : Listener transition from START to STATE1
... : Listener stateExited in START
... : Listener stateEntered in STATE1
... : Action Source: START, State: STATE1, Target: STATE1, Event: START_STATE1
... : Action Source: START, State: STATE1, Target: STATE1, Event: START_STATE1
... : Listener stateChanged from START to STATE1
... : Listener transitionEnded from START to STATE1

Estas son las dependencias necesarias a añadir n la herramienta de construcción y la salida en la terminal de las trazas ejecutando la máquina de estados desde el estado inicial e inicializada desde un estado en concreto.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
plugins {
id 'java'
id 'application'
}
mainClassName = 'io.github.picodotdev.blogbitix.springstatemachine.Main'
ext {
versions = [
springBoot: '2.1.3.RELEASE',
]
}
repositories {
jcenter()
}
dependencies {
implementation platform("org.springframework.boot:spring-boot-dependencies:$versions.springBoot")
compile("org.springframework.boot:spring-boot-starter")
compile("org.springframework.boot:spring-boot-autoconfigure")
compile("org.springframework.statemachine:spring-statemachine-core:2.1.1.RELEASE")
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
 . ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.3.RELEASE)
...
... : Starting Main on archlinux with PID 32679 (/home/picodotdev/Software/personal/blog-ejemplos/SpringStatemachine/build/classes/java/main started by picodotdev in /home/picodotdev/Software/personal/blog-ejemplos/SpringStatemachine)
... : No active profile set, falling back to default profiles: default
... : Initializing ExecutorService
... : Started Main in 0.719 seconds (JVM running for 0.948)
... : Listener transitionStarted from null to START
... : Listener stateContext (Message Payload: null, Message Headers: {id=8b954f2b-aeef-48dd-a4ca-e799c188d476, timestamp=1554007267639}, Variables: {}
... : Listener transition from null to START
... : Listener stateContext (Message Payload: null, Message Headers: {id=e1bf1cb1-c597-4975-6874-580b8d7c6a0a, timestamp=1554007267640}, Variables: {}
... : Listener stateEntered in START
... : Listener stateContext (Message Payload: null, Message Headers: {id=e938b91d-bada-c56b-d845-5f3958bba4f8, timestamp=1554007267641}, Variables: {}
... : Listener stateChanged from null to START
... : Listener stateContext (Message Payload: null, Message Headers: {id=b58324de-f43d-417b-cc38-86efdea7274f, timestamp=1554007267641}, Variables: {}
... : Listener stateMachineStarted
... : Listener stateContext (Message Payload: null, Message Headers: {id=747d233f-0d46-0bd0-78be-6cf026739dd8, timestamp=1554007267642}, Variables: {}
... : Listener transitionEnded from null to START
... : Listener stateContext (Message Payload: null, Message Headers: {id=1fe8b3a2-e176-4791-dcab-d8759b2e4dc6, timestamp=1554007267642}, Variables: {}
... : started org.springframework.statemachine.support.DefaultStateMachineExecutor@51e37590
... : started STATE1 TASKS TASK13 TASK11 TASK12 TASK21 TASK23 TASK22 CHOICE1 CHOICE2 START STATE2 END JOIN FORK CHOICE / START / uuid=a2882db4-b636-4fac-93d0-87c50434401e / id=null
... : Listener extendedStateChanged variable to 42
... : Listener stateContext (Message Payload: null, Message Headers: {id=afa8655f-09a8-a551-c11e-36a36cd2c92a, timestamp=1554007267643}, Variables: {variable=42}
... : Listener transitionStarted from START to STATE1
... : Listener stateContext (Message Payload: START_STATE1, Message Headers: {key=31, id=bf50ec37-b6f7-32d5-fd59-292d7f1ea265, timestamp=1554007267643}, Variables: {variable=42}
... : Action Source: START, State: START, Target: STATE1, Event: START_STATE1
... : Listener transition from START to STATE1
... : Listener stateContext (Message Payload: START_STATE1, Message Headers: {key=31, id=bf50ec37-b6f7-32d5-fd59-292d7f1ea265, timestamp=1554007267643}, Variables: {variable=42}
... : Listener stateExited in START
... : Listener stateContext (Message Payload: START_STATE1, Message Headers: {key=31, id=bf50ec37-b6f7-32d5-fd59-292d7f1ea265, timestamp=1554007267643}, Variables: {variable=42}
... : Listener stateEntered in STATE1
... : Listener stateContext (Message Payload: START_STATE1, Message Headers: {key=31, id=bf50ec37-b6f7-32d5-fd59-292d7f1ea265, timestamp=1554007267643}, Variables: {variable=42}
... : Action Source: START, State: STATE1, Target: STATE1, Event: START_STATE1
... : Action Source: START, State: STATE1, Target: STATE1, Event: START_STATE1
... : Listener stateChanged from START to STATE1
... : Listener stateContext (Message Payload: START_STATE1, Message Headers: {key=31, id=bf50ec37-b6f7-32d5-fd59-292d7f1ea265, timestamp=1554007267643}, Variables: {variable=42}
... : Listener transitionEnded from START to STATE1
... : Listener stateContext (Message Payload: START_STATE1, Message Headers: {key=31, id=bf50ec37-b6f7-32d5-fd59-292d7f1ea265, timestamp=1554007267643}, Variables: {variable=42}
... : Listener transitionStarted from STATE1 to CHOICE
... : Listener stateContext (Message Payload: STATE1_CHOICE, Message Headers: {id=8c969225-fec1-6603-63dd-7ae00f8ead10, timestamp=1554007267650}, Variables: {variable=42}
... : Listener transition from STATE1 to CHOICE
... : Listener stateContext (Message Payload: STATE1_CHOICE, Message Headers: {id=8c969225-fec1-6603-63dd-7ae00f8ead10, timestamp=1554007267650}, Variables: {variable=42}
... : Action Source: STATE1, State: STATE1, Target: CHOICE, Event: STATE1_CHOICE
... : Listener stateExited in STATE1
... : Listener stateContext (Message Payload: STATE1_CHOICE, Message Headers: {id=8c969225-fec1-6603-63dd-7ae00f8ead10, timestamp=1554007267650}, Variables: {variable=42}
... : Listener stateEntered in CHOICE1
... : Listener stateContext (Message Payload: STATE1_CHOICE, Message Headers: {id=8c969225-fec1-6603-63dd-7ae00f8ead10, timestamp=1554007267650}, Variables: {variable=42}
... : Listener stateChanged from STATE1 to CHOICE1
... : Listener stateContext (Message Payload: STATE1_CHOICE, Message Headers: {id=8c969225-fec1-6603-63dd-7ae00f8ead10, timestamp=1554007267650}, Variables: {variable=42}
... : Listener transitionEnded from STATE1 to CHOICE
... : Listener stateContext (Message Payload: STATE1_CHOICE, Message Headers: {id=8c969225-fec1-6603-63dd-7ae00f8ead10, timestamp=1554007267650}, Variables: {variable=42}
... : Listener transitionStarted from CHOICE1 to FORK
... : Listener stateContext (Message Payload: CHOICE1_FORK, Message Headers: {id=1f2f3a68-19e1-29d4-8677-2c63c5e360a3, timestamp=1554007267651}, Variables: {variable=42}
... : Listener transition from CHOICE1 to FORK
... : Listener stateContext (Message Payload: CHOICE1_FORK, Message Headers: {id=1f2f3a68-19e1-29d4-8677-2c63c5e360a3, timestamp=1554007267651}, Variables: {variable=42}
... : Listener stateExited in CHOICE1
... : Listener stateContext (Message Payload: CHOICE1_FORK, Message Headers: {id=1f2f3a68-19e1-29d4-8677-2c63c5e360a3, timestamp=1554007267651}, Variables: {variable=42}
... : Listener stateEntered in TASKS
... : Listener stateContext (Message Payload: CHOICE1_FORK, Message Headers: {id=1f2f3a68-19e1-29d4-8677-2c63c5e360a3, timestamp=1554007267651}, Variables: {variable=42}
... : Listener transitionStarted from null to TASK11
... : Listener stateContext (Message Payload: null, Message Headers: {id=37715a25-6933-14b8-2e25-756a447a3630, timestamp=1554007267652}, Variables: {variable=42}
... : Listener transition from null to TASK11
... : Listener stateContext (Message Payload: null, Message Headers: {id=62093291-042b-cd75-1405-4a96be2b5682, timestamp=1554007267652}, Variables: {variable=42}
... : Listener stateEntered in TASK11
... : Listener stateContext (Message Payload: null, Message Headers: {id=a7c34605-3515-c56f-ff7a-8977d2331fd9, timestamp=1554007267652}, Variables: {variable=42}
... : Listener stateChanged from null to TASK11
... : Listener stateContext (Message Payload: null, Message Headers: {id=7b000dca-0fb2-f95a-a6db-aaf6600e1f5f, timestamp=1554007267653}, Variables: {variable=42}
... : Listener stateMachineStarted
... : Listener stateContext (Message Payload: null, Message Headers: {id=908bcc73-0466-dec2-14b6-2822ad56c7ba, timestamp=1554007267653}, Variables: {variable=42}
... : Listener transitionEnded from null to TASK11
... : Listener stateContext (Message Payload: null, Message Headers: {id=e262f0fe-6047-9638-b592-c6e4bbc4e2ef, timestamp=1554007267653}, Variables: {variable=42}
... : started org.springframework.statemachine.support.DefaultStateMachineExecutor@5a865416
... : started TASK13 TASK11 TASK12 / TASK11 / uuid=b703837b-1a49-4dc4-83aa-5420f6cf0517 / id=null#8dd81b65-183c-43d0-8d27-c5ff71d2968a
... : Listener transitionStarted from null to TASK21
... : Listener stateContext (Message Payload: null, Message Headers: {id=fe645a0b-2b28-8734-e09a-271a7df04d17, timestamp=1554007267653}, Variables: {variable=42}
... : Listener transition from null to TASK21
... : Listener stateContext (Message Payload: null, Message Headers: {id=ab063429-8d97-27ce-c25b-3d8488bbc03f, timestamp=1554007267653}, Variables: {variable=42}
... : Listener stateEntered in TASK21
... : Listener stateContext (Message Payload: null, Message Headers: {id=309a6395-2545-89c6-833f-ba49a7893e2f, timestamp=1554007267654}, Variables: {variable=42}
... : Listener stateChanged from null to TASK21
... : Listener stateContext (Message Payload: null, Message Headers: {id=014531de-2bb4-efc8-1b8c-1bfcaf6f445c, timestamp=1554007267654}, Variables: {variable=42}
... : Listener stateMachineStarted
... : Listener stateContext (Message Payload: null, Message Headers: {id=1375e1a3-b78f-7efb-84ed-3536e7198715, timestamp=1554007267654}, Variables: {variable=42}
... : Listener transitionEnded from null to TASK21
... : Listener stateContext (Message Payload: null, Message Headers: {id=4fe6a6a2-f279-33fb-58fa-14731e111b70, timestamp=1554007267654}, Variables: {variable=42}
... : started org.springframework.statemachine.support.DefaultStateMachineExecutor@6dc1484
... : started TASK21 TASK23 TASK22 / TASK21 / uuid=74691d25-219e-4dca-83ff-2e80ffc74f93 / id=null#5260c53d-1cbb-47a6-984a-8ef1ceb3ab31
... : Listener stateChanged from CHOICE1 to TASKS
... : Listener stateContext (Message Payload: CHOICE1_FORK, Message Headers: {id=1f2f3a68-19e1-29d4-8677-2c63c5e360a3, timestamp=1554007267651}, Variables: {variable=42}
... : Listener transitionEnded from CHOICE1 to FORK
... : Listener stateContext (Message Payload: CHOICE1_FORK, Message Headers: {id=1f2f3a68-19e1-29d4-8677-2c63c5e360a3, timestamp=1554007267651}, Variables: {variable=42}
... : Listener transitionStarted from TASK11 to TASK12
... : Listener stateContext (Message Payload: TASK11_TASK12, Message Headers: {id=a5f14519-e027-16b9-e718-7884633feb01, timestamp=1554007267655}, Variables: {variable=42}
... : Listener transition from TASK11 to TASK12
... : Listener stateContext (Message Payload: TASK11_TASK12, Message Headers: {id=a5f14519-e027-16b9-e718-7884633feb01, timestamp=1554007267655}, Variables: {variable=42}
... : Listener stateExited in TASK11
... : Listener stateContext (Message Payload: TASK11_TASK12, Message Headers: {id=a5f14519-e027-16b9-e718-7884633feb01, timestamp=1554007267655}, Variables: {variable=42}
... : Listener stateEntered in TASK12
... : Listener stateContext (Message Payload: TASK11_TASK12, Message Headers: {id=a5f14519-e027-16b9-e718-7884633feb01, timestamp=1554007267655}, Variables: {variable=42}
... : Listener stateChanged from TASK11 to TASK12
... : Listener stateContext (Message Payload: TASK11_TASK12, Message Headers: {id=a5f14519-e027-16b9-e718-7884633feb01, timestamp=1554007267655}, Variables: {variable=42}
... : Listener transitionEnded from TASK11 to TASK12
... : Listener stateContext (Message Payload: TASK11_TASK12, Message Headers: {id=a5f14519-e027-16b9-e718-7884633feb01, timestamp=1554007267655}, Variables: {variable=42}
... : Listener eventNotAccepted GenericMessage [payload=TASK11_TASK12, headers={id=a5f14519-e027-16b9-e718-7884633feb01, timestamp=1554007267655}]
... : Listener stateContext (Message Payload: TASK11_TASK12, Message Headers: {id=a5f14519-e027-16b9-e718-7884633feb01, timestamp=1554007267655}, Variables: {variable=42}
... : Listener eventNotAccepted GenericMessage [payload=TASK21_TASK22, headers={id=2f048198-ef29-0b11-53d9-c22202b3a023, timestamp=1554007267656}]
... : Listener stateContext (Message Payload: TASK21_TASK22, Message Headers: {id=2f048198-ef29-0b11-53d9-c22202b3a023, timestamp=1554007267656}, Variables: {variable=42}
... : Listener eventNotAccepted GenericMessage [payload=TASK21_TASK22, headers={id=2f048198-ef29-0b11-53d9-c22202b3a023, timestamp=1554007267656}]
... : Listener stateContext (Message Payload: TASK21_TASK22, Message Headers: {id=2f048198-ef29-0b11-53d9-c22202b3a023, timestamp=1554007267656}, Variables: {variable=42}
... : Listener transitionStarted from TASK21 to TASK22
... : Listener stateContext (Message Payload: TASK21_TASK22, Message Headers: {id=2f048198-ef29-0b11-53d9-c22202b3a023, timestamp=1554007267656}, Variables: {variable=42}
... : Listener transition from TASK21 to TASK22
... : Listener stateContext (Message Payload: TASK21_TASK22, Message Headers: {id=2f048198-ef29-0b11-53d9-c22202b3a023, timestamp=1554007267656}, Variables: {variable=42}
... : Listener stateExited in TASK21
... : Listener stateContext (Message Payload: TASK21_TASK22, Message Headers: {id=2f048198-ef29-0b11-53d9-c22202b3a023, timestamp=1554007267656}, Variables: {variable=42}
... : Listener stateEntered in TASK22
... : Listener stateContext (Message Payload: TASK21_TASK22, Message Headers: {id=2f048198-ef29-0b11-53d9-c22202b3a023, timestamp=1554007267656}, Variables: {variable=42}
... : Listener stateChanged from TASK21 to TASK22
... : Listener stateContext (Message Payload: TASK21_TASK22, Message Headers: {id=2f048198-ef29-0b11-53d9-c22202b3a023, timestamp=1554007267656}, Variables: {variable=42}
... : Listener transitionEnded from TASK21 to TASK22
... : Listener stateContext (Message Payload: TASK21_TASK22, Message Headers: {id=2f048198-ef29-0b11-53d9-c22202b3a023, timestamp=1554007267656}, Variables: {variable=42}
... : Listener eventNotAccepted GenericMessage [payload=TASK12_TASK13, headers={id=1fd4d183-88d0-f30e-34a8-530c23e66b23, timestamp=1554007267657}]
... : Listener stateContext (Message Payload: TASK12_TASK13, Message Headers: {id=1fd4d183-88d0-f30e-34a8-530c23e66b23, timestamp=1554007267657}, Variables: {variable=42}
... : Listener eventNotAccepted GenericMessage [payload=TASK12_TASK13, headers={id=1fd4d183-88d0-f30e-34a8-530c23e66b23, timestamp=1554007267657}]
... : Listener stateContext (Message Payload: TASK12_TASK13, Message Headers: {id=1fd4d183-88d0-f30e-34a8-530c23e66b23, timestamp=1554007267657}, Variables: {variable=42}
... : Listener transitionStarted from TASK12 to TASK13
... : Listener stateContext (Message Payload: TASK12_TASK13, Message Headers: {id=1fd4d183-88d0-f30e-34a8-530c23e66b23, timestamp=1554007267657}, Variables: {variable=42}
... : Listener transition from TASK12 to TASK13
... : Listener stateContext (Message Payload: TASK12_TASK13, Message Headers: {id=1fd4d183-88d0-f30e-34a8-530c23e66b23, timestamp=1554007267657}, Variables: {variable=42}
... : Listener stateExited in TASK12
... : Listener stateContext (Message Payload: TASK12_TASK13, Message Headers: {id=1fd4d183-88d0-f30e-34a8-530c23e66b23, timestamp=1554007267657}, Variables: {variable=42}
... : Listener stateEntered in TASK13
... : Listener stateContext (Message Payload: TASK12_TASK13, Message Headers: {id=1fd4d183-88d0-f30e-34a8-530c23e66b23, timestamp=1554007267657}, Variables: {variable=42}
... : Listener stateChanged from TASK12 to TASK13
... : Listener stateContext (Message Payload: TASK12_TASK13, Message Headers: {id=1fd4d183-88d0-f30e-34a8-530c23e66b23, timestamp=1554007267657}, Variables: {variable=42}
... : stopped org.springframework.statemachine.support.DefaultStateMachineExecutor@5a865416
... : Listener stateMachineStopped
... : Listener stateContext (Message Payload: null, Message Headers: {id=887ca4d7-226b-9f99-3955-dce7151dcbae, timestamp=1554007267659}, Variables: {variable=42}
... : stopped TASK13 TASK11 TASK12 / / uuid=b703837b-1a49-4dc4-83aa-5420f6cf0517 / id=null#8dd81b65-183c-43d0-8d27-c5ff71d2968a
... : Listener transitionEnded from TASK12 to TASK13
... : Listener stateContext (Message Payload: TASK12_TASK13, Message Headers: {id=1fd4d183-88d0-f30e-34a8-530c23e66b23, timestamp=1554007267657}, Variables: {variable=42}
... : Listener eventNotAccepted GenericMessage [payload=TASK22_TASK23, headers={id=592d65e8-6ccc-b942-0d10-d5b9b645c558, timestamp=1554007267659}]
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=592d65e8-6ccc-b942-0d10-d5b9b645c558, timestamp=1554007267659}, Variables: {variable=42}
... : Listener eventNotAccepted GenericMessage [payload=TASK22_TASK23, headers={id=592d65e8-6ccc-b942-0d10-d5b9b645c558, timestamp=1554007267659}]
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=592d65e8-6ccc-b942-0d10-d5b9b645c558, timestamp=1554007267659}, Variables: {variable=42}
... : Listener transitionStarted from TASK22 to TASK23
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=592d65e8-6ccc-b942-0d10-d5b9b645c558, timestamp=1554007267659}, Variables: {variable=42}
... : Listener transition from TASK22 to TASK23
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=592d65e8-6ccc-b942-0d10-d5b9b645c558, timestamp=1554007267659}, Variables: {variable=42}
... : Listener stateExited in TASK22
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=592d65e8-6ccc-b942-0d10-d5b9b645c558, timestamp=1554007267659}, Variables: {variable=42}
... : Listener stateEntered in TASK23
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=592d65e8-6ccc-b942-0d10-d5b9b645c558, timestamp=1554007267659}, Variables: {variable=42}
... : Listener transitionStarted from TASK13 to JOIN
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=592d65e8-6ccc-b942-0d10-d5b9b645c558, timestamp=1554007267659}, Variables: {variable=42}
... : Listener transition from TASK13 to JOIN
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=592d65e8-6ccc-b942-0d10-d5b9b645c558, timestamp=1554007267659}, Variables: {variable=42}
... : Listener stateExited in TASK13
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=592d65e8-6ccc-b942-0d10-d5b9b645c558, timestamp=1554007267659}, Variables: {variable=42}
... : Listener transitionEnded from TASK13 to JOIN
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=592d65e8-6ccc-b942-0d10-d5b9b645c558, timestamp=1554007267659}, Variables: {variable=42}
... : Listener transitionStarted from TASK23 to JOIN
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=592d65e8-6ccc-b942-0d10-d5b9b645c558, timestamp=1554007267659}, Variables: {variable=42}
... : Listener transition from TASK23 to JOIN
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=592d65e8-6ccc-b942-0d10-d5b9b645c558, timestamp=1554007267659}, Variables: {variable=42}
... : Listener stateExited in TASK23
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=592d65e8-6ccc-b942-0d10-d5b9b645c558, timestamp=1554007267659}, Variables: {variable=42}
... : Listener transitionEnded from TASK23 to JOIN
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=592d65e8-6ccc-b942-0d10-d5b9b645c558, timestamp=1554007267659}, Variables: {variable=42}
... : stopped org.springframework.statemachine.support.DefaultStateMachineExecutor@6dc1484
... : Listener stateMachineStopped
... : Listener stateContext (Message Payload: null, Message Headers: {id=b65b9ec8-ef97-dba7-aa77-44b8b5588ff9, timestamp=1554007267662}, Variables: {variable=42}
... : stopped TASK21 TASK23 TASK22 / / uuid=74691d25-219e-4dca-83ff-2e80ffc74f93 / id=null#5260c53d-1cbb-47a6-984a-8ef1ceb3ab31
... : Listener stateExited in TASKS
... : Listener stateContext (Message Payload: null, Message Headers: {id=2664aa9e-1ec2-593f-3016-81231e9551a2, timestamp=1554007267662}, Variables: {variable=42}
... : Listener stateEntered in STATE2
... : Listener stateContext (Message Payload: null, Message Headers: {id=8510c1b9-6b2f-46ae-9412-9542b0098fec, timestamp=1554007267662}, Variables: {variable=42}
... : Listener stateChanged from TASKS to STATE2
... : Listener stateContext (Message Payload: null, Message Headers: {id=8d650465-62b7-f71a-070e-9b1dadf29861, timestamp=1554007267662}, Variables: {variable=42}
... : Listener stateChanged from TASK22 to TASK23
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=592d65e8-6ccc-b942-0d10-d5b9b645c558, timestamp=1554007267659}, Variables: {variable=42}
... : Listener transitionEnded from TASK22 to TASK23
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=592d65e8-6ccc-b942-0d10-d5b9b645c558, timestamp=1554007267659}, Variables: {variable=42}
... : Listener transitionStarted from STATE2 to END
... : Listener stateContext (Message Payload: STATE2_END, Message Headers: {id=f84d435c-bc74-e23d-f0b5-3191241118ef, timestamp=1554007267663}, Variables: {variable=42}
... : Listener transition from STATE2 to END
... : Listener stateContext (Message Payload: STATE2_END, Message Headers: {id=f84d435c-bc74-e23d-f0b5-3191241118ef, timestamp=1554007267663}, Variables: {variable=42}
... : Listener stateExited in STATE2
... : Listener stateContext (Message Payload: STATE2_END, Message Headers: {id=f84d435c-bc74-e23d-f0b5-3191241118ef, timestamp=1554007267663}, Variables: {variable=42}
... : Listener stateEntered in END
... : Listener stateContext (Message Payload: STATE2_END, Message Headers: {id=f84d435c-bc74-e23d-f0b5-3191241118ef, timestamp=1554007267663}, Variables: {variable=42}
... : Listener stateChanged from STATE2 to END
... : Listener stateContext (Message Payload: STATE2_END, Message Headers: {id=f84d435c-bc74-e23d-f0b5-3191241118ef, timestamp=1554007267663}, Variables: {variable=42}
... : stopped org.springframework.statemachine.support.DefaultStateMachineExecutor@51e37590
... : Listener stateMachineStopped
... : Listener stateContext (Message Payload: null, Message Headers: {id=cf95710c-ecb5-3df6-61e3-11799f0925d7, timestamp=1554007267664}, Variables: {variable=42}
... : stopped STATE1 TASKS TASK13 TASK11 TASK12 TASK21 TASK23 TASK22 CHOICE1 CHOICE2 START STATE2 END JOIN FORK CHOICE / / uuid=a2882db4-b636-4fac-93d0-87c50434401e / id=null
... : Listener transitionEnded from STATE2 to END
... : Listener stateContext (Message Payload: STATE2_END, Message Headers: {id=f84d435c-bc74-e23d-f0b5-3191241118ef, timestamp=1554007267663}, Variables: {variable=42}
...
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
... : started ObjectState [getIds()=[TASK11], getClass()=class org.springframework.statemachine.state.ObjectState, hashCode()=1584833211, toString()=AbstractState [id=TASK11, pseudoState=org.springframework.statemachine.state.DefaultPseudoState@757194dc, deferred=[], entryActions=[], exitActions=[], stateActions=[], regions=[], submachine=null]]
... : started ObjectState [getIds()=[TASK22], getClass()=class org.springframework.statemachine.state.ObjectState, hashCode()=800456240, toString()=AbstractState [id=TASK22, pseudoState=null, deferred=[], entryActions=[], exitActions=[], stateActions=[], regions=[], submachine=null]]
... : started RegionState [getIds()=[TASKS, TASK11, TASK22], getClass()=class org.springframework.statemachine.state.RegionState, hashCode()=1311544814, toString()=AbstractState [id=TASKS, pseudoState=org.springframework.statemachine.state.DefaultPseudoState@d0ec63, deferred=[], entryActions=[], exitActions=[], stateActions=[], regions=[TASK13 TASK11 TASK12 / TASK11 / uuid=b703837b-1a49-4dc4-83aa-5420f6cf0517 / id=null, TASK21 TASK23 TASK22 / TASK22 / uuid=74691d25-219e-4dca-83ff-2e80ffc74f93 / id=null], submachine=null]]
... : started org.springframework.statemachine.support.DefaultStateMachineExecutor@51e37590
... : Listener stateMachineStarted
... : Listener stateContext (Message Payload: null, Message Headers: {id=a6be7fdd-8685-5b21-1f34-9dcf718ec374, timestamp=1554007267665}, Variables: {}
... : started org.springframework.statemachine.support.DefaultStateMachineExecutor@5a865416
... : Listener stateMachineStarted
... : Listener stateContext (Message Payload: null, Message Headers: {id=3eef6870-ee58-fa1d-8798-71987d336914, timestamp=1554007267665}, Variables: {}
... : started TASK13 TASK11 TASK12 / TASK11 / uuid=b703837b-1a49-4dc4-83aa-5420f6cf0517 / id=null
... : started org.springframework.statemachine.support.DefaultStateMachineExecutor@6dc1484
... : Listener stateMachineStarted
... : Listener stateContext (Message Payload: null, Message Headers: {id=aef5d00b-59ea-8eed-81ff-c840b1ce2b94, timestamp=1554007267666}, Variables: {}
... : started TASK21 TASK23 TASK22 / TASK22 / uuid=74691d25-219e-4dca-83ff-2e80ffc74f93 / id=null
... : started STATE1 TASKS TASK13 TASK11 TASK12 TASK21 TASK23 TASK22 CHOICE1 CHOICE2 START STATE2 END JOIN FORK CHOICE / TASKS,TASK11,TASK22 / uuid=a2882db4-b636-4fac-93d0-87c50434401e / id=null
... : Listener transitionStarted from TASK11 to TASK12
... : Listener stateContext (Message Payload: TASK11_TASK12, Message Headers: {id=dd47c963-48b4-a47b-093a-bd52cc4371f8, timestamp=1554007267666}, Variables: {}
... : Listener transition from TASK11 to TASK12
... : Listener stateContext (Message Payload: TASK11_TASK12, Message Headers: {id=dd47c963-48b4-a47b-093a-bd52cc4371f8, timestamp=1554007267666}, Variables: {}
... : Listener stateExited in TASK11
... : Listener stateContext (Message Payload: TASK11_TASK12, Message Headers: {id=dd47c963-48b4-a47b-093a-bd52cc4371f8, timestamp=1554007267666}, Variables: {}
... : Listener stateEntered in TASK12
... : Listener stateContext (Message Payload: TASK11_TASK12, Message Headers: {id=dd47c963-48b4-a47b-093a-bd52cc4371f8, timestamp=1554007267666}, Variables: {}
... : Listener stateChanged from TASK11 to TASK12
... : Listener stateContext (Message Payload: TASK11_TASK12, Message Headers: {id=dd47c963-48b4-a47b-093a-bd52cc4371f8, timestamp=1554007267666}, Variables: {}
... : Listener transitionEnded from TASK11 to TASK12
... : Listener stateContext (Message Payload: TASK11_TASK12, Message Headers: {id=dd47c963-48b4-a47b-093a-bd52cc4371f8, timestamp=1554007267666}, Variables: {}
... : Listener eventNotAccepted GenericMessage [payload=TASK11_TASK12, headers={id=dd47c963-48b4-a47b-093a-bd52cc4371f8, timestamp=1554007267666}]
... : Listener stateContext (Message Payload: TASK11_TASK12, Message Headers: {id=dd47c963-48b4-a47b-093a-bd52cc4371f8, timestamp=1554007267666}, Variables: {}
... : Listener eventNotAccepted GenericMessage [payload=TASK22_TASK23, headers={id=285c9e5d-930d-c150-0ef0-2de6cec651ab, timestamp=1554007267667}]
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=285c9e5d-930d-c150-0ef0-2de6cec651ab, timestamp=1554007267667}, Variables: {}
... : Listener eventNotAccepted GenericMessage [payload=TASK22_TASK23, headers={id=285c9e5d-930d-c150-0ef0-2de6cec651ab, timestamp=1554007267667}]
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=285c9e5d-930d-c150-0ef0-2de6cec651ab, timestamp=1554007267667}, Variables: {}
... : Listener transitionStarted from TASK22 to TASK23
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=285c9e5d-930d-c150-0ef0-2de6cec651ab, timestamp=1554007267667}, Variables: {}
... : Listener transition from TASK22 to TASK23
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=285c9e5d-930d-c150-0ef0-2de6cec651ab, timestamp=1554007267667}, Variables: {}
... : Listener stateExited in TASK22
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=285c9e5d-930d-c150-0ef0-2de6cec651ab, timestamp=1554007267667}, Variables: {}
... : Listener stateEntered in TASK23
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=285c9e5d-930d-c150-0ef0-2de6cec651ab, timestamp=1554007267667}, Variables: {}
... : Listener stateChanged from TASK22 to TASK23
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=285c9e5d-930d-c150-0ef0-2de6cec651ab, timestamp=1554007267667}, Variables: {}
... : stopped org.springframework.statemachine.support.DefaultStateMachineExecutor@6dc1484
... : Listener stateMachineStopped
... : Listener stateContext (Message Payload: null, Message Headers: {id=ff2f4d56-fde7-2ab0-c412-e5307b52cfc1, timestamp=1554007267668}, Variables: {}
... : stopped TASK21 TASK23 TASK22 / / uuid=74691d25-219e-4dca-83ff-2e80ffc74f93 / id=null
... : Listener transitionEnded from TASK22 to TASK23
... : Listener stateContext (Message Payload: TASK22_TASK23, Message Headers: {id=285c9e5d-930d-c150-0ef0-2de6cec651ab, timestamp=1554007267667}, Variables: {}
... : Listener eventNotAccepted GenericMessage [payload=TASK12_TASK13, headers={id=5ff52736-4f77-7bf1-e0ad-9e9cef73e1aa, timestamp=1554007267668}]
... : Listener stateContext (Message Payload: TASK12_TASK13, Message Headers: {id=5ff52736-4f77-7bf1-e0ad-9e9cef73e1aa, timestamp=1554007267668}, Variables: {}
... : Listener eventNotAccepted GenericMessage [payload=TASK12_TASK13, headers={id=5ff52736-4f77-7bf1-e0ad-9e9cef73e1aa, timestamp=1554007267668}]
... : Listener stateContext (Message Payload: TASK12_TASK13, Message Headers: {id=5ff52736-4f77-7bf1-e0ad-9e9cef73e1aa, timestamp=1554007267668}, Variables: {}
... : Listener transitionStarted from TASK12 to TASK13
... : Listener stateContext (Message Payload: TASK12_TASK13, Message Headers: {id=5ff52736-4f77-7bf1-e0ad-9e9cef73e1aa, timestamp=1554007267668}, Variables: {}
... : Listener transition from TASK12 to TASK13
... : Listener stateContext (Message Payload: TASK12_TASK13, Message Headers: {id=5ff52736-4f77-7bf1-e0ad-9e9cef73e1aa, timestamp=1554007267668}, Variables: {}
... : Listener stateExited in TASK12
... : Listener stateContext (Message Payload: TASK12_TASK13, Message Headers: {id=5ff52736-4f77-7bf1-e0ad-9e9cef73e1aa, timestamp=1554007267668}, Variables: {}
... : Listener stateEntered in TASK13
... : Listener stateContext (Message Payload: TASK12_TASK13, Message Headers: {id=5ff52736-4f77-7bf1-e0ad-9e9cef73e1aa, timestamp=1554007267668}, Variables: {}
... : stopped org.springframework.statemachine.support.DefaultStateMachineExecutor@5a865416
... : Listener stateMachineStopped
... : Listener stateContext (Message Payload: null, Message Headers: {id=ee20242f-e669-8719-8cef-869d2c7bbb13, timestamp=1554007267669}, Variables: {}
... : stopped TASK13 TASK11 TASK12 / / uuid=b703837b-1a49-4dc4-83aa-5420f6cf0517 / id=null
... : Listener stateExited in TASKS
... : Listener stateContext (Message Payload: null, Message Headers: {id=9835d889-b3e4-b056-2e8d-1dfc3850907e, timestamp=1554007267670}, Variables: {}
... : Listener stateEntered in STATE2
... : Listener stateContext (Message Payload: null, Message Headers: {id=e9f3c6bb-b384-455e-354c-7cc6d8b2f6a1, timestamp=1554007267670}, Variables: {}
... : Listener stateChanged from TASKS to STATE2
... : Listener stateContext (Message Payload: null, Message Headers: {id=6c0826b9-8ec5-84a8-f6d4-53b1c3311334, timestamp=1554007267670}, Variables: {}
... : Listener stateChanged from TASK12 to TASK13
... : Listener stateContext (Message Payload: TASK12_TASK13, Message Headers: {id=5ff52736-4f77-7bf1-e0ad-9e9cef73e1aa, timestamp=1554007267668}, Variables: {}
... : Listener transitionEnded from TASK12 to TASK13
... : Listener stateContext (Message Payload: TASK12_TASK13, Message Headers: {id=5ff52736-4f77-7bf1-e0ad-9e9cef73e1aa, timestamp=1554007267668}, Variables: {}
... : Listener transitionStarted from STATE2 to END
... : Listener stateContext (Message Payload: STATE2_END, Message Headers: {id=6be8a081-daec-2aca-268b-3f0af8940247, timestamp=1554007267670}, Variables: {}
... : Listener transition from STATE2 to END
... : Listener stateContext (Message Payload: STATE2_END, Message Headers: {id=6be8a081-daec-2aca-268b-3f0af8940247, timestamp=1554007267670}, Variables: {}
... : Listener stateExited in STATE2
... : Listener stateContext (Message Payload: STATE2_END, Message Headers: {id=6be8a081-daec-2aca-268b-3f0af8940247, timestamp=1554007267670}, Variables: {}
... : Listener stateEntered in END
... : Listener stateContext (Message Payload: STATE2_END, Message Headers: {id=6be8a081-daec-2aca-268b-3f0af8940247, timestamp=1554007267670}, Variables: {}
... : Listener stateChanged from STATE2 to END
... : Listener stateContext (Message Payload: STATE2_END, Message Headers: {id=6be8a081-daec-2aca-268b-3f0af8940247, timestamp=1554007267670}, Variables: {}
... : stopped org.springframework.statemachine.support.DefaultStateMachineExecutor@51e37590
... : Listener stateMachineStopped
... : Listener stateContext (Message Payload: null, Message Headers: {id=3825827b-07cb-6df6-f6bb-ebb391030e1e, timestamp=1554007267671}, Variables: {}
... : stopped STATE1 TASKS TASK13 TASK11 TASK12 TASK21 TASK23 TASK22 CHOICE1 CHOICE2 START STATE2 END JOIN FORK CHOICE / / uuid=a2882db4-b636-4fac-93d0-87c50434401e / id=null
... : Listener transitionEnded from STATE2 to END
... : Listener stateContext (Message Payload: STATE2_END, Message Headers: {id=6be8a081-daec-2aca-268b-3f0af8940247, timestamp=1554007267670}, Variables: {}
... : destroy called

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradle run.

» Leer más, comentarios, etc...

proyectos Ágiles

Master en Agile – MMA 2019

marzo 31, 2019 02:09

En octubre de 2019 se iniciará el Barcelona una nueva edición del Postgrado en Métodos Ágiles (PMA) y otra del Máster en Transformación Agile (MTA) en La Salle (Universitat Ramon Llull), el primero a nivel mundial sobre Agile.

MMA-banner

Tras 6 exitosas ediciones, este año cuenta con diversas novedades,  entre las que se incluyen, dentro del PMA, el Certified Scrum Master (CSM) de la Scrum Alliance y el Team Kanban Practitioner (TKP) de la Lean Kanban University.

SAI_BadgeSizes_DigitalBadging_CSMbadge-TKP-EDU-2

Otras novedades, dentro del MTA, son los contenidos sobre Visual Management Framework (VMF- Agile para cualquier área) y escalado con Kanban.

Esta es una oportunidad única para aprender de profesionales de primer nivel, con varios años de experiencia específica en Agile, aplicando principios y métodos ágiles en contextos diversos, especializándose en aspectos concretos,  investigando sobre nuevas técnicas y ponentes en conferencias nacionales e incluso internacionales.

PMA – Postgrado en métodos Ágiles

Asignaturas Temas Profesores
Fundamentos & Inception

Principios y métodos más conocidos (Scrum, Lean, Kanban y XP). Facilitadores e impedimentos.

Inception y conceptualización ágil de proyecto.

Silvia Sistaré
Agustín Yagüe
Scrum y Kanban Estimación y planificación ágil, framework de Scrum, retrospectivas, Kanban, métricas ágiles, herramientas ágiles físicas, radiadores de información. Tiago Garcez
Teodora Bozheva
Personas y equipos Gestión de personas, gestión de conflictos, motivación e incentivos, facilitación compartida, contratación ágil, facilitación gráfica. Steven Wallace

Manuel Lopez

Silvia Sistaré

Agile Product Management

Design Thinking.

Lean Startup & Agile Product Management

Ángel Díaz-Maroto

Gabriel Prat

Ingeniería Ágil, TDD, BDD, Legacy code 

User eXperience y prototipado en Agile.

ALM ágil, eXtreme Programing, Software Craftsmanship, testing ágil.

Desarrollo guiado por pruebas (de aceptación y unitarias).

Cómo trabajar con código heredado y reducir la deuda técnica.

Marc Pifarré

Álvaro García

Pablo Gómez

Rubén Bernárdez

Trabajo Final de Postgrado Durante el Postgrado se elaborará un caso práctico de introducción de los contenidos en una empresa, organizándose en equipos multidisciplinares de alumnos utilizando Scrum.  

 

MMA – Master en Métodos Ágiles

Este máster está especializado en Business Agility, agilidad organizacional y transformación. Incluye todas las asignaturas del Postgrado (PMA) y, adicionalmente, las siguientes:

Asignaturas Temas Profesores
Enterprise Learning & personal efficiency

Comunidades de Práctica, Open Spaces, Talent development, gamification.

Productividad y aprendizaje personal.

Steven Wallace
Manuel Lopez
Jordi Molla
Lean Thinking & Agile Management

Lean y escalado con Kanban

Visual Management Framework (VMF- Agile para cualquier área)

Agile-Lean Management

Teodora Bozheva

Xavier Quesada

Xavier Albaladejo

Coaching y Cultura

Coaching de equipos, liderazgo.

Tipos de cultura empresarial, gestión del cambio organizativo.

Joserra Díaz

Jasmina Nikolic
Jaume Gurt

Continuous Transformation

Enterprise continuous improvement.

Agile Rollout. Contratos ágiles.

Enterprise software development & DevOps

Ángel Medinilla
Xavier Albaladejo

Álvaro García

Scaling Agile 

Escalado (LESS, Spotify, Nexus, SAFe), desescalado y auto-organización empresarial (reinventing organizations, sociocracy, …).

Impact Mapping, Product Portfolio Management, Roadmapping, Budgeting for Agile

Adrian Perreau
Fernando Palomo

Mattijas Larsson

Trabajo Final de Máster Durante el Máster se elaborará un caso práctico de introducción y aplicación de Agile en una empresa, incluyendo la parte de transformación organizativa y de cultura  

Algunos comentarios de los alumnos en ediciones anteriores: “Cuando se acaba la clase, estamos ya esperando a que llegue la semana que viene para ver un nuevo tema con el siguiente profesor”. “Muy práctico”. “La calidad y diversidad de puntos de vista entre los profesores le aporta mucho valor”.  Más detalles en: Mejora de la situación laboral los alumnos del PMA trans un año.

 
Además de los conocimientos concretos que se reciben, uno de los principales cambios que han experimentado los alumnos es mayor perspectiva en los problemas, cómo abordar la complejidad en las empresas, en su trabajo y en las relaciones entre personas y en equipos. Han ganado discurso y aplomo para defender de manera más objetiva propuestas de cambio y mejora.
 

El Máster tendrá una duración de un año académico y se realizará viernes tarde y sábado por la mañana.

 
Para más detalles, consultar la página oficial del Máster.
 

–> Ver también cuál ha sido la Mejora de la situación laboral de los alumnos tras un año y los Principales aspectos valorados por los alumnos.

» Leer más, comentarios, etc...

Mascando Bits

Cómo subir un paquete a PyPI

marzo 28, 2019 07:05

Recientemente un compañero mío quería distribuir una pequeña librería en Python que había escrito. En lugar de forzar a los desarrolladores a clonar su repositorio, quería que pudieran instalarse la librería con un sólo comando “pip install“. Planteamiento muy interesante que se formaliza con el concepto de gestor de paquetes.

Un gestor permite un uso más inteligente, ágil y seguro de las dependencias y ayuda tanto al que distribuye el paquete como al que lo usa, resolviendo problemas como el versionado, la cadena de dependencias dentro del paquete, la instalación…

Quisiera a fin de fomentar el uso del gestor de paquetes de Python llamado pip, que usa como repositorio oficial PyPI (Python Package Index), generar una guía lo más completa posible. ⚠ Me gustaría remarcar que con ello no quiero ni pretendo incentivar que cualquiera se dedique a subir de cualquier forma lo primero que se lo ocurra. Aludo a la responsabilidad de cada uno, para no convertir PyPI en un vertedero de paquetes de dudosa calidad. ⚠

¿Qué es PyPI?

Desde la web oficial:

PyPI — Índice de Paquetes Python

El Índice de Paquetes Python es un repositorio de software para el lenguaje de programación de Python.

¿Has programado algo genial? ¿Quieres que otros puedan instalarlo con easy_install o pip? Pon tu código en PyPI. Es una gran lista de paquetes de Python donde debes enviar tu paquete para que pueda instalarse fácilmente con uno sólo comando.

La buena noticia es que enviar un paquete a PyPI en la teoría es muy simple: registrarte y cargar tu código, todo de manera gratuita. La mala noticia es que en la práctica es un poco más complicado que eso 😅. La otra buena noticia es que he escrito esta guía 😁 y que, si estás atascado, siempre puedes consultar la documentación oficial 😉.

He escrito esta guía con los siguientes supuestos:

  •      El módulo / biblioteca / paquete que está enviando se llama mypackage.
  •      mypackage está alojado en GitHub.

Crear tu cuenta

Crea una cuenta en PyPI Live y también en PyPI Test. Debes crear una cuenta para poder cargar tu código. Te recomiendo usar el mismo correo electrónico y contraseña para ambas cuentas, sólo para hacerte la vida más fácil cuando llegue el momento de subir tu código. Ambas plataformas son idénticas, siendo la de test una réplica de la oficial para que pruebes a subir tus paquetes.

Crear un archivo de configuración .pypirc

Este archivo contiene su información para la autenticación con PyPI, tanto la versión en vivo como la versión de prueba.

[distutils]
index-servers =
  pypi
  pypitest

[pypi]
repository=https://upload.pypi.org/legacy/
username=your_username
password=your_password

[pypitest]
repository=https://test.pypi.org/legacy/
username=your_username
password=your_password

Esto es sólo para hacer tu vida más fácil, para que cuando llegue el momento de subir el código de de tu paquete no tengas que recordar/escribir tu nombre de usuario y contraseña. Asegúrate de poner este archivo en tu carpeta de inicio de tu sistema GNU/Linux; tu ruta debe ser:

~/.pypirc

Debido a que este archivo contiene tu nombre de usuario y contraseña, es posible que desees cambiar sus permisos para que sólo tú puedas leerlos y escribirlos. Desde la terminal, ejecuta:

chmod 600 ~/.pypirc

Si por el contrario te encuentras en un sistema Windows genera el archivo .pypirc en la carpeta de tu usuario en C:\Users\usuario con el siguiente comando:

type nul > your_file.txt
👁 Notas sobre usernames / passwords

En Python 3, si tu contraseña incluye un % sin procesar, debes generar una secuencia de escape duplicándolo. El analizador de configuración de .pypirc interpola las cadenas de texto. Por ejemplo, si tu contraseña es hello%world:

[pypi]
repository=https://pypi.python.org/pypi
username=myusername
password=hello%%world

Nunca me he encontrado con este problema, pero si te ocurre, esto podría ayudar. 😉

Este comportamiento de escape ha sido parcheado y ya no es necesario hacerlo, pero si ves un error con un código de respuesta de:

 403: Invalid or non-existent authentication information 

Intenta eliminar la secuencia de escape de los signos de porcentaje en tu contraseña.

Si tu contraseña incluye espacios, asegúrese de no entrecomillarla. Por ejemplo, si tu contraseña es me encanta Mascando Bits:

[pypi]
repository=https://pypi.python.org/pypi
username=myusername
password=me encanta MascandoBits

Preparar el paquete

Cada paquete en PyPI necesita tener un archivo llamado setup.py en la raíz del directorio. Si estás utilizando un archivo README en formato markdown, también necesitará un archivo MANIFEST.in. Además, es recomendable elegir una licencia para tu paquete reflejada en un archivo LICENSE que describa lo que se puede hacer con tu código. Entonces, si por ejemplo he estado trabajando en una biblioteca llamada mypackage, la estructura de mi directorio se vería tal que así:

root-dir/   # nombre de directorio de trabajo aleatorio
  setup.py
  MANIFEST.in
  LICENSE.txt
  README.md
  mypackage/
    __init__.py
    foo.py
    bar.py
    baz.py

A continuación un desglose de lo que va en cada archivo:

setup.py

Son los metadatos de la librería necesarios para generar el paquete.

from setuptools import setup

setup(
    name='mypackage',
    packages=['mypackage'], # Mismo nombre que en la estructura de carpetas de arriba
    version='0.1',
    license='LGPL v3', # La licencia que tenga tu paqeute
    description='A random test lib',
    author='RDCH106',
    author_email='contact@rdch106.hol.es',
    url='https://github.com/RDCH106/mypackage', # Usa la URL del repositorio de GitHub
    download_url='https://github.com/RDCH106/parallel_foreach_submodule/archive/v0.1.tar.gz', # Te lo explico a continuación
    keywords='test example develop', # Palabras que definan tu paquete
    classifiers=['Programming Language :: Python',  # Clasificadores de compatibilidad con versiones de Python para tu paqeute
                 'Programming Language :: Python :: 2.7',
                 'Programming Language :: Python :: 3.3',
                 'Programming Language :: Python :: 3.4',
                 'Programming Language :: Python :: 3.5',
                 'Programming Language :: Python :: 3.6',
                 'Programming Language :: Python :: 3.7'],
)

El parámetro download_url es un enlace a un archivo alojado con el código de tu repositorio. Github alojará esto para ti, pero solo si creas una etiqueta git tag. En tu repositorio, escribe en la línea de comandos:

git tag v0.1 -m "Agrega una etiqueta para que podamos poner esto en PyPI"

Luego, escribe el siguiente comando para mostrar la lista de etiquetas:

git tag

Deberías ver v0.1 en la lista.

Por último escribe el siguiente comando para actualizar tu código en Github con la información de etiqueta más reciente:

git push --tags origin master

Github creará archivos comprimidos para descargar en https://github.com/{username}/{package_name}/archive/v{tag}.tar.gz.

MANIFEST.in

Incluye archivos que se empaquetarán en al distribución del paquete.

include LICENSE.txt README.md

LICENSE.txt será el archivo que contiene la licencia y README.md será el fichero que contenga la información básica (instalación, uso…) que quieras distribuir con tu paquete.

Subir tu paquete a PyPI Test

Ejecuta:

python setup.py sdist upload -r pypitest

No debería recibir ningún error si seguiste los pasos hasta este punto, y ahora también deberías poder ver tu paquete en el repositorio PyPI de prueba. Si hay algo que no te gusta este es el momento de hacer los cambios que quieras. Eso sí, para subir el paquete de nuevo tendrás que editar el número de versión para que sea distinto al de la anterior vez.

⚠ PyPI te permite borrar las versiones del paquete y subir todos las versiones que quieras, pero no editar una versión de tu paquete subida. Esto es para mantener la estabilidad de las dependencias. Es posible retirar una versión, creando la consecuente ruptura de dependencias, pero no se deja modificar para una versión de un paquete por no desvirtuar el sentido mismo del versionado.

Es posible que en algunas guía o documentación hayas visto que también se hace previo al upload un comando register:

python setup.py register -r pypitest

Ya no es necesario y no está soportado:

https://packaging.python.org/guides/migrating-to-pypi-org/#registering-package-names-metadata


Subir tu paquete a PyPI Live

Ejecuta:

python setup.py sdist upload -r pypi

Y ya, !lo has hecho! 🎉🎉🎉 Has publicado tu primer paquete en PyPI y podrá ser instalado con el gestor de paquetes pip o cualquier otro como conda.

» Leer más, comentarios, etc...

Meta-Info

¿Que es?

Planeta Código es un agregador de weblogs sobre programación y desarrollo en castellano. Si eres lector te permite seguirlos de modo cómodo en esta misma página o mediante el fichero de subscripción.

rss subscripción

Puedes utilizar las siguientes imagenes para enlazar PlanetaCodigo:
planetacodigo

planetacodigo

Si tienes un weblog de programación y quieres ser añadido aquí, envíame un email solicitándolo.

Idea: Juanjo Navarro

Diseño: Albin