Translate

jueves, 10 de febrero de 2022

Type class en Scala


Type class es un patrón de programación que se origina en Haskell. Nos permiten ampliar las bibliotecas existentes con nuevas funciones, sin utilizar la herencia tradicional y sin alterar el código fuente de la biblioteca original.

Type class es una especie de interfaz que define algún tipo de comportamiento. Si un tipo es miembro de una clase de tipos, significa que ese tipo soporta e implementa el comportamiento que define la clase de tipos. La gente que viene de lenguajes orientados a objetos es propensa a confundir las clases de tipos porque piensan que son como las clases en los lenguajes orientados a objetos. Bien, pues no lo son. Una aproximación más adecuada sería pensar que son como las interfaces de Java, o los protocolos de Objective-C, pero mejor.

Veamos un ejemplo: 

ghci> :t (==)

(==) :: (Eq a) => a -> a -> Bool

Interesante. Aquí vemos algo nuevo, el símbolo =>. Cualquier cosa antes del símbolo => es una restricción de clase. Podemos leer la declaración de tipo anterior como: la función de igualdad toma dos parámetros que son del mismo tipo y devuelve un Bool. El tipo de estos dos parámetros debe ser miembro de la clase Eq (esto es la restricción de clase).

El tipo de clase Eq proporciona una interfaz para las comparaciones de igualdad. Cualquier tipo que tenga sentido comparar dos valores de ese tipo por igualdad debe ser miembro de la clase Eq. Todos los tipos estándar de Haskell excepto el tipo IO (un tipo para manejar la entrada/salida) y las funciones forman parte de la clase Eq.

Hay tres componentes importantes para implementar este patrón en Scala. Type class en Scala se implementan usando valores y parámetros implícitos y, opcionalmente, usando clases implícitas. Las construcciones del lenguaje Scala corresponden a los componentes de los type class de la siguiente manera:

  • traits: type classes;
  • implicit values: instancia del type class;
  • implicit parameters: donde se usa el type class use
  • implicit classes: es opcional, y facilita el uso de type class
Veamos estos puntos en más detalle : 

traits : Una clase de tipo es una interfaz o API que representa alguna funcionalidad que queremos implementar. En Scala, una clase de tipo está representada por un rasgo o traits con al menos un parámetro de tipo. Por ejemplo, podemos representar el comportamiento genérico de "serializar a JSON" de la siguiente manera:

// Define a very simple JSON AST
sealed trait Json
  final case class JsObject(get: Map[String, Json]) extends Json
  final case class JsString(get: String) extends Json
  final case class JsNumber(get: Double) extends Json
  final case object JsNull extends Json

// The "serialize to JSON" behaviour is encoded in this trait
trait JsonWriter[A] {
  def write(value: A): Json
}

JsonWriter es nuestra clase de tipo en este ejemplo, con Json y sus subtipos proporcionando código de soporte. Cuando lleguemos a implementar instancias de JsonWriter, el parámetro de tipo A será el tipo concreto de datos que estamos escribiendo.

Las instancias de una clase de tipo proporcionan implementaciones de la clase de tipo para tipos específicos que nos interesan, que pueden incluir tipos de la biblioteca estándar de Scala y tipos de nuestro modelo de dominio.

En Scala, definimos instancias creando implementaciones concretas de la clase de tipo y etiquetándolas con la palabra clave implícita:

final case class Person(name: String, email: String)
  object JsonWriterInstances {
    implicit val stringWriter: JsonWriter[String] =
      new JsonWriter[String] {
        def write(value: String): Json =
          JsString(value)
      }

implicit val personWriter: JsonWriter[Person] =
  new JsonWriter[Person] {
    def write(value: Person): Json =
      JsObject(Map(
        "name" -> JsString(value.name),
        "email" -> JsString(value.email)
      ))
  }

//etc 
}

Puff me quedo relargo el post, seguimos con implicit parameters y implicit classes en el próximo post. 

lunes, 7 de febrero de 2022

Registrar un servicio Spring boot en Eureka

 


En el post anterior levantamos un servidor Eureka, ahora vamos registrar un servicio. 

Lo primero que debe hacer es agregar la dependencia Spring Eureka al archivo pom.xml del servicio que queremos registrar :

<dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-starter-eureka</artifactId>

</dependency>

El artefacto spring-cloud-starter-eureka contiene los archivos jar que Spring Cloud usará para interactuar con su servicio Eureka.

Después de configurar su archivo pom.xml, debe indicarle a Spring Boot que registre el servicio en el servidor Eureka. Este registro se realice tenemos que configurarlo con el archivo src/main/java/resources/application.yml 

spring:
  application:
    name: myService
  profiles:
    active:
       default
    cloud:
      config:
        enabled: true
eureka:
  instance:
    preferIpAddress: true
  client:
    registerWithEureka: true
    fetchRegistry: true
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

Todo servicio registrado en Eureka tendrá dos componentes asociados: el ID de la aplicación y el ID de la instancia. El ID de la aplicación se utiliza para representar una instancia de servicio de grupo. En un microservicio basado en Spring-Boot, el ID de la aplicación siempre será el valor establecido por la propiedad spring.application.name. Para su servicio de organización, su spring.application.name se llama creativamente servicio de organización. El ID de la instancia será un número aleatorio destinado a representar una sola instancia de servicio.

La segunda parte de la configuración proporciona cómo y dónde debe registrarse el servicio con el servicio Eureka. La propiedad eureka.instance.preferIpAddress le dice a Eureka que desea registrar la dirección IP del servicio en Eureka en lugar de su nombre de host.

El atributo eureka.client.registerWithEureka es el disparador para decirle al servicio de la organización que se registre en Eureka. El eureka.client.fetchRegistry se utiliza para indicarle al cliente Spring Eureka que obtenga una copia local del registro. Establecer este atributo en true, almacenará en caché el registro localmente en lugar de llamar al servicio Eureka con cada búsqueda. Cada 30 segundos, el software del cliente volverá a ponerse en contacto con el servicio de Eureka para cualquier cambio en el registro.

El último atributo, el atributo eureka.serviceUrl.defaultZone, contiene una lista separada por comas de los servicios de Eureka que el cliente usará para resolver la ubicación del servicio. Para este ejemplo, solo tendrá un servicio Eureka.

Se puede usar la API REST de Eureka para ver el contenido del registro. para ver todos los
instancias de un servicio, presione el siguiente punto final GET:

http://<servicio de eureka>:8761/eureka/apps/<ID DE APLICACIÓN>

Por ejemplo, para ver el servicio de organización en el registro, puede llamar a 

http://localhost:8761/eureka/apps/myservice.

El formato predeterminado que devuelve el servicio Eureka es XML. 

jueves, 3 de febrero de 2022

Monix


Monix es una librería Scala/Scala.js de alto rendimiento para componer programas asincrónicos basados en eventos.


Monix, un proyecto de Typelevel, que ejemplifica la programación funcional pura, tipificada en Scala, sin comprometer el rendimiento.

  • Como características podemos nombrar : 
  • expone tipos de datos Observable, Iterant, Task y Coeval, junto con todo el soporte que necesitan
  • usa solo lo que necesita
  • diseñado para una verdadera asincronía, ejecutándose tanto en JVM como en Scala.js
  • excelente cobertura de prueba, calidad de código y documentación de API como política principal del proyecto

El proyecto comenzó como una implementación adecuada de ReactiveX, con influencias de programación funcional más fuertes y diseñado desde cero para la contrapresión y creado para interactuar limpiamente con la biblioteca estándar de Scala, compatible de forma inmediata con el protocolo Reactive Streams. Luego se expandió para incluir abstracciones para suspender los efectos secundarios y para el manejo de recursos, siendo uno de los padres e implementadores de Cats Effect.

Dejo link: https://monix.io/

martes, 1 de febrero de 2022

Service discovery usando Spring y Netflix Eureka


Ahora vamos a implementar el descubrimiento de servicios configurando un agente de descubrimiento de servicios y luego registrando dos servicios con el agente. Luego, un servicio llamará a otro servicio utilizando la información recuperada por el descubrimiento de servicios. Spring Cloud ofrece múltiples métodos para buscar información de un agente de descubrimiento de servicios. También analizaremos las fortalezas y debilidades de cada enfoque.

Una vez más, el proyecto Spring Cloud hace que este tipo de configuración sea trivial de realizar. Vamos a utilizar Spring Cloud y el motor de descubrimiento de servicios Eureka de Netflix para implementar el patrón de descubrimiento de servicios. Como balanceador de carga del lado del cliente, utilizaremos Spring Cloud y las bibliotecas Ribbon de Netflix.

Vamos a tener que seguir los siguentes pasos:

  1. A medida que los servicios se inician se registrarán en el Servicio Eureka. Este proceso de registro le indicará a Eureka la ubicación física y el número de puerto de cada instancia de servicio junto con una ID de servicio para el servicio que se está iniciando.
  2. Cuando un servicio llame a otro, utilizará Ribbon, que se comunicará con el servicio de Eureka para recuperar la información de ubicación del servicio y luego almacenarla esto en la caché localmente.
  3. Periódicamente, la biblioteca de Netflix Ribbon hará ping al servicio Eureka y actualizará su memoria caché local de ubicaciones de servicio.

Cualquier nueva instancia de servicios será visible, mientras que cualquier instancia que no esté en buen estado se eliminará de la memoria caché local.

A continuación, implementará este diseño configurando su servicio Spring Cloud Eureka. 

Para empezar vamos a agregar la dependencia a Eureka, si usamos maven tenemos que agregar : 

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-eureka-server</artifactId>

</dependency>

Ahora tenemos que configurar nuestro server, en el archivo application.yml : 

server:
   port: 8761
 
eureka:
   client:
      registerWithEureka: false
      fetchRegistry: false
   server:
       waitTimeInMsWhenSyncEmpty:5

Las propiedades clave que se establecen son el atributo server.port que establece el puerto predeterminado utilizado para el servicio Eureka. El atributo eureka.client.registerWithEureka le dice al servicio que no se registre con un servicio Eureka cuando se inicia la aplicación Spring Boot Eureka porque este es el servicio Eureka. El atributo eureka.client.fetchRegistry se establece en falso para que cuando se inicie el servicio Eureka, no intente almacenar en caché su información de registro localmente. Cuando ejecute un cliente de Eureka, querrá cambiar este valor para los servicios de Spring Boot que se registrarán con Eureka.

Notará que el último atributo, eureka.server.waitTimeInMsWhenSyncEmpty , está comentado. Cuando esté probando su servicio localmente, debe descomentar esta línea porque Eureka no anunciará de inmediato ningún servicio que se registre con él. Esperará cinco minutos de forma predeterminada para dar a todos los servicios la oportunidad de registrarse antes de anunciarlos. Eliminar los comentarios de esta línea para las pruebas locales ayudará a acelerar la cantidad de tiempo que tardará el servicio Eureka en iniciarse y mostrar los servicios registrados en él.

El registro de servicios individuales tardará hasta 30 segundos en aparecer en el servicio Eureka porque Eureka requiere tres pings de latidos consecutivos del servicio separados por 10 segundos antes de decir que el servicio está listo para usarse. Tenga esto en cuenta cuando implemente y pruebe sus propios servicios.

El último trabajo de configuración que va a realizar para configurar su servicio Eureka es agregar una anotación a la clase de arranque de la aplicación que está utilizando para iniciar su servicio Eureka.

package com.assembly.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {

   public static void main(String[] args) {
      SpringApplication.run(EurekaServerApplication.class, args);
   }

}

Y ya esta, tenemos nuestro server Eureka andando. En post posteriores vamos a registrar nuestros servicios. 

Scala With Cats

Me tope con el libro : Scala with Cats 2 que es gratuito, por lo que les dejo una reseña y a bajarlo y estudiar: 


El objetivo principal de este libro es enseñar arquitectura y diseño de sistemas utilizando las técnicas de la programación funcional moderna. Esto significa diseñar sistemas como pequeñas unidades componibles, expresando restricciones e interacciones a través del sistema de tipos y utilizando la composición para guiar la construcción de grandes sistemas de una manera que mantenga la visión arquitectónica original.

El libro también sirve como una introducción a la biblioteca Cats. Usando abstracciones de Cats y explicando la estructura de Cats para que pueda usarla. Las ideas generales no son específicas de Cats, pero Cats proporciona una excelente implementación que es beneficiosa para aprender.

Dejo link: https://www.scalawithcats.com/

sábado, 29 de enero de 2022

Concurrencia en Clojure - parte 7


Seguimos con concurrencia en Clojure

Ahora podemos modificar nuestro servicio web para usar sesiones. Primero, necesitamos una función que cree una nueva sesión:

(defn create-session []

  (let [snippets (repeatedly promise)

    translations (delay (map translate

                         (strings->sentences (map deref snippets))))]

      (new-session {:snippets snippets :translations translations})))


Seguimos usando una secuencia perezosa infinita de promesas para representar los fragmentos entrantes y un mapa sobre esa secuencia para representar las traducciones, pero ahora ambos están almacenados en una sesión.

A continuación, debemos modificar accept-snippet y get-translation para buscar :snippets o :translations dentro de una sesión:

(defn accept-snippet [session n text]

  (deliver (nth (:snippets session) n) text))

(defn get-translation [session n]

  @(nth @(:translations session) n))


Finalmente, definimos las rutas que vinculan estas funciones a las URI:

(defroutes app-routes
  (POST "/session/create" []
    (response (str (create-session))))

  (context "/session/:session-id" [session-id]
    (let [session (get-session (edn/read-string session-id))]
      (routes
        (PUT "/snippet/:n" [n :as {:keys [body]}]
          (accept-snippet session (edn/read-string n) (slurp body))
          (response "OK"))

        (GET "/translation/:n" [n]
          (response (get-translation session (edn/read-string n))))))))

Esto nos brinda un servicio web que hace un uso juicioso de los datos mutables pero aún se siente principalmente funcional.


jueves, 27 de enero de 2022

Concurrencia en Clojure - parte 6



Vamos a crear un servicio web que maneje múltiples transcripciones introduciendo el concepto de una sesión. Cada sesión tiene un identificador numérico único, que se genera de la siguiente manera:

(def last-session-id (atom 0))

(defn next-session-id []

(swap! last-session-id inc))

Esto usa un átomo, last-session-id , que se incrementa cada vez que queremos una nueva ID de sesión. Como resultado, cada vez que se llama a next-session-id, devuelve un número uno más alto que el anterior:

server.core=> (in-ns 'server.session)
#<Namespace server.session>
server.session=> (next-session-id)
1
server.session=> (next-session-id)
2
server.session=> (next-session-id)
3

Vamos a hacer un seguimiento de las sesiones activas con otro átomo llamado sesiones que contiene un mapa de ID de sesión y valores de sesión:

(def sessions (atom {}))

(defn new-session [initial]

  (let [session-id (next-session-id)]

    (swap! sessions assoc session-id initial)

     session-id))


(defn get-session [id]

   (@sessions id))

Creamos una nueva sesión pasando un valor inicial a new-session, que obtiene una nueva ID de sesión y la agrega a las sesiones llamando a swap!. Recuper una sesión con get-session es una simple cuestión de buscarlo por su ID.

Si no vamos a aumentar continuamente la cantidad de memoria que usamos, necesitaremos alguna forma de eliminar las sesiones cuando ya no estén en uso. Nosotros podría hacer esto explícitamente (quizás con una función de eliminación de sesión), pero dado que estamos escribiendo un servicio web en el que no necesariamente podemos confiar en que los clientes limpien correctamente, vamos a implementar la caducidad de la sesión (expiry) en su lugar. Esto requiere un pequeño cambio en el código anterior:

(def sessions (atom {}))

(defn now []

  (System/currentTimeMillis))


(defn new-session [initial]

  (let [session-id (next-session-id)

         session (assoc initial :last-referenced (atom (now)))]

    (swap! sessions assoc session-id session)

    session-id))


(defn get-session [id]

  (let [session (@sessions id)]

     (reset! (:last-referenced session) (now))

     session))


Hemos agregado una función llamada now que devuelve la hora actual. Cuando new-session crea una sesión, agrega una entrada :last-referenced a la sesión, otro átomo que contiene la hora actual. Esto se actualiza con reset! cada vez que get-session accede a la sesión.

Ahora que cada sesión tiene una entrada :last-referenced, podemos caducar sesiones verificando periódicamente si alguna no ha sido referenciada por más de un cierto período de tiempo:


(defn session-expiry-time []

  (- (now) (* 10 60 1000)))


(defn expired? [session]

  (< @(:last-referenced session) (session-expiry-time)))


(defn sweep-sessions []

  (swap! sessions #(remove-vals % expired?)))


(def session-sweeper

  (schedule {:min (range 0 60 5)} sweep-sessions))


Esto utiliza la biblioteca Schejulure para crear un barrido de sesiones, que se ejecuten una vez cada cinco minutos. Cada vez que se ejecuta, elimina cualquier sesión que haya expirado.

miércoles, 26 de enero de 2022

Concurrencia en Clojure - parte 5




Imagina que queremos tener un átomo que nunca tenga un valor negativo. Podemos garantizar esto por medio de una función de validación cuando creamos el átomo:

user=> (def non-negative (atom 0 :validator #(>= % 0)))

#'user/non-negative

user=> (reset! non-negative 42)

42

user=> (reset! non-negative -1)

IllegalStateException Invalid reference state

Un validador es una función que se llama cada vez que se intenta cambiar el valor del átomo. Si devuelve verdadero, el intento puede tener éxito, pero si devuelve falso, el intento se abandonará.

El validador se llama antes de que se haya cambiado el valor del átomo y, al igual que la función que se pasa a swap!. Por lo tanto, los validadores tampoco deben tener efectos secundarios.

Los átomos también pueden tener observadores asociados con ellos:

user=> (def a (atom 0))

#'user/a

user=> (add-watch a :print #(println "Changed from " %3 " to " %4))

#<Atom@542ab4b1: 0>

user=> (swap! a + 2)

Changed from 0 to 2

2

La clave se utiliza para identificar al observador (así, por ejemplo, si hay varios observadores, podemos eliminar uno específico proporcionando la clave correspondiente). La función watch se llama cada vez que cambia el valor del átomo. Se le dan cuatro argumentos: la clave que se le dio a add-watch, una referencia al átomo, el valor anterior y el nuevo valor.

En el código anterior, usamos la macro lectora #(...) nuevamente para definir una función anónima que imprime los valores antiguo (%3) y nuevo (%4) del átomo.

A diferencia de los validadores, las funciones de observación se llaman después de que el valor ha cambiado y solo se llamarán una vez, ¡sin importar la frecuencia con la que se intercambien! 

Por lo tanto, una función watch puede tener efectos secundarios. Que en el momento en que se llama a la función de observación, es posible que el valor del átomo ya haya cambiado nuevamente, por lo que las funciones de observación siempre deben usar los valores pasados como argumentos y nunca eliminar la referencia del átomo.


martes, 25 de enero de 2022

Concurrencia en Clojure - parte 4


Seguimos con concurrencia en Clojure

Las estructuras de datos persistentes son invaluables para la programación concurrente porque una vez que un subproceso tiene una referencia a una estructura de datos, no verá cambios realizados por ningún otro subproceso. Las estructuras de datos persistentes separan la identidad del estado. ¿Cuál es el nivel de combustible en su automóvil? En este momento, podría estar medio lleno. Algún tiempo después estará casi vacío, y unos minutos después de eso (después de que te detengas para llenar) estará lleno. La identidad "nivel de combustible en su automóvil" es una cosa, cuyo estado cambia constantemente. El “nivel de combustible en su automóvil” es en realidad una secuencia de diferentes valores: el 23/02/2012 a las 12:03 era 0,53; en 2012-02-23 14:30 era 0.12; y en 2012-02-23 14:31 era 1.00.

Una variable en un lenguaje imperativo completa (entrelaza, interconecta) la identidad y el estado: una única identidad solo puede tener un único valor, lo que hace que sea fácil perder de vista el hecho de que el estado es realmente una secuencia de valores a lo largo del tiempo. Las estructuras de datos persistentes separan la identidad del estado: si recuperamos el estado actual asociado con una identidad, ese estado es inmutable e muatble, sin importar lo que suceda con la identidad de la que recuperamos en el futuro.

Heráclito lo expresó así: No podías meterte dos veces en el mismo río; porque otras aguas están siempre fluyendo sobre ti. 

La mayoría de los lenguajes se aferran a la falacia de que el río es una sola entidad consistente; Clojure reconoce que está en constante cambio.

Debido a que Clojure es funcional, los átomos pueden no tener bloqueo; internamente, utilizan el método compareAndSet() en java.util.concurrent.AtomicReference. Eso significa que son muy rápidos y no se bloquean (por lo que no hay peligro de interbloqueo). Pero también significa ese intercambio necesita manejar el caso en el que el valor del átomo ha sido cambiado por otro hilo entre llamar a la función para generar un nuevo valor e intentar cambiar ese valor.

Si eso sucede, volverá a intentarlo. Descartará el valor devuelto por la función y lo llamará de nuevo con el nuevo valor del átomo. Ya vimos algo muy similar a esto cuando usamos ConcurrentHashMap. Esto significa que es esencial que la función pase a swap! no tiene efectos secundarios; si los tuviera, entonces esos efectos secundarios podrían ocurrir más de una vez. Afortunadamente, aquí es donde la naturaleza funcional de Clojure vale la pena: el código funcional naturalmente no tiene efectos secundarios.

lunes, 24 de enero de 2022

Concurrencia en Clojure - parte 3


Seguimos con concurrencia en Clojure

Las estructuras de datos persistentes, no tiene nada que ver con la persistencia en el disco o dentro de una base de datos. En cambio, se refiere a una estructura de datos que siempre conserva su versión anterior cuando se modifica, lo que permite que el código tenga una vista consistente de los datos ante modificaciones. 

Podemos ver esto fácilmente en el REPL:

user=> (def mapv1 {:name "paul" :age 45})

#'user/mapv1

user=> (def mapv2 (assoc mapv1 :sex :male))

#'user/mapv2

user=> mapv1

{:age 45, :name "paul"}

user=> mapv2

{:age 45, :name "paul", :sex :male}


Las estructuras de datos persistentes se comportan como si se hiciera una copia completa cada vez que se modifican. Si esa fuera la forma en que se implementaron realmente, sería muy ineficiente y, por lo tanto, de uso limitado 

Afortunadamente, la implementación es mucho más inteligente que eso y hace uso de la estructura compartida.

La estructura de datos persistentes más fácil de entender es la lista. Aquí hay una lista simple:

user=> (def listv1 (list 1 2 3))

#'user/listv1

user=> listv1

(1 2 3)


Y aquí hay un diagrama de cómo se ve en la memoria:



Ahora vamos a crear una versión modificada con cons, que devuelve una copia de la lista con un elemento agregado al frente:

user=> (def listv2 (cons 4 listv1))

#'user/listv2

user=> listv2

(4 1 2 3)

La nueva lista puede compartir toda la lista anterior, sin necesidad de copiar:

Finalmente, creemos otra versión modificada:

user=> (def listv3 (cons 5 (rest listv1)))

#'user/listv3

user=> listv3

(5 2 3)




En este caso, la nueva lista solo hace uso de una parte del original, pero aún no es necesaria la copia.

No siempre podemos evitar copiar. Las listas solo manejan bien las colas comunes: si queremos tener dos listas con colas diferentes, no tenemos más remedio que copiar.

Aquí hay un ejemplo:

user=> (def listv1 (list 1 2 3 4))
#'user/listv1
user=> (def listv2 (take 2 listv1))
#'user/listv2
user=> listv2
(1 2)

Todas las colecciones de Clojure son persistentes. Los vectores, mapas y conjuntos persistentes son más complejos de implementar que las listas, pero para nuestros propósitos todo lo que necesitamos saber es que comparten estructura y que proporcionan límites de rendimiento similares a sus equivalentes no persistentes en lenguajes como Ruby y Java.


Concurrencia en Clojure - parte 2


Seguimos con concurrencia en Clojure

Veamos un ejemplo que muestra cómo las estructuras de datos persistentes de Clojure significan que el estado mutable no puede escapar como lo hace en Java.

(def players (atom ()))

(defn list-players []

 (response (json/encode @players)))


(defn create-player [player-name]

  (swap! players conj player-name)

  (status (response "") 201))


(defroutes app-routes
  (GET "/players" [] (list-players))
  (PUT "/players/:player-name" [player-name] (create-player player-name)))

(defn -main [& args]
  (run-jetty (site app-routes) {:port 3000}))

Esto define un par de rutas: una solicitud GET a /players recuperará una lista de los jugadores actuales (en formato JSON) y una solicitud PUT a "/players/:player-name" agregará un jugador a esa lista. El servidor Embedded Jetty es multiproceso, por lo que nuestro código deberá ser seguro para subprocesos.

Veámoslo en acción. Podemos ejecutarlo desde la línea de comandos con curl:

$ curl localhost:3000/players

[]

$ curl -X put localhost:3000/players/john

$ curl localhost:3000/players

["john"]

$ curl -X put localhost:3000/players/paul

$ curl -X put localhost:3000/players/george

$ curl -X put localhost:3000/players/ringo

$ curl localhost:3000/players

["ringo","george","paul","john"]

Ahora veamos cómo funciona este código. El átomo de los jugadores :
  (def players (atom ()))
 se inicializa en la lista vacía (). Se agrega un nuevo jugador a la lista con conj :
  (swap! players conj player-name)
, y se devuelve una respuesta vacía con un estado HTTP 201 (creado). La lista de jugadores se devuelve mediante la codificación JSON del resultado de obtener el valor de los jugadores con @:
 (response (json/encode @players)))

Todo esto parece muy simple (y lo es), pero algo podría estar preocupándote algo al respecto. Tanto las funciones de listar jugadores como las de crear jugadores acceden a los jugadores. ¿Por qué este código no sufre el mismo problema que el código Java? ¿Qué sucede si un subproceso agrega una entrada a la lista de reproductores mientras otro lo itera y lo convierte a JSON? Este código es seguro para subprocesos porque las estructuras de datos de Clojure son persistentes.

Y que significa que sea persistente? y en el próximo post te cuento. 

martes, 18 de enero de 2022

La arquitectura del service discovery


Para comenzar nuestra discusión sobre la arquitectura de descubrimiento de servicios o service discovery, debemos comprender cuatro conceptos. Estos conceptos generales se comparten en todas las implementaciones de detección de servicios:

  • Registro de servicios: ¿Cómo se registra un servicio con el agente de detección de servicios?
  • Búsqueda del cliente de la dirección del servicio: ¿cuál es el medio por el cual un cliente del servicio busca información del servicio?
  • Información compartida: ¿Cómo se comparte la información del servicio entre los nodos?
  • Supervisión del estado: ¿cómo comunican los servicios su estado al agente de detección de servicios?

A medida que se inician las instancias de servicio, registrarán su ubicación física, ruta y puerto a los que se puede acceder con una o más instancias de detección de servicios. Si bien cada instancia de un servicio tendrá una dirección IP y un puerto únicos, cada instancia de servicio que surja se registrará con la misma ID de servicio. Un ID de servicio no es más que una clave que identifica de forma única a un grupo de las mismas instancias de servicio.

Por lo general, un servicio solo se registrará con una instancia de servicio de detección de servicios. La mayoría de las implementaciones de detección de servicios utilizan un modelo de propagación de datos de igual a igual en el que los datos de cada instancia de servicio se comunican a todos los demás nodos del clúster.

Dependiendo de la implementación del descubrimiento de servicios, el mecanismo de propagación podría usar una lista codificada de servicios para propagar o usar un protocolo de multidifusión. Como el protocolo "gossip" o "infection-style" para permitir que otros nodos "descubran" cambios en el clúster.

Finalmente, el servicio de detección de servicios empujará o sacará de su estado cada instancia de servicio. Cualquier servicio que no devuelva una buena verificación de estado se eliminará del conjunto de instancias de servicio disponibles.

Una vez que un servicio se ha registrado en un servicio de descubrimiento de servicios, está listo para ser utilizado por una aplicación o servicio que necesita usar sus capacidades. Existen diferentes modelos para que un cliente “descubra” un servicio. Un cliente puede confiar únicamente en el motor de descubrimiento de servicios para resolver las ubicaciones de los servicios cada vez que se llama a un servicio. Con este enfoque, el motor de detección de servicios se invocará cada vez que se realice una llamada a una instancia de microservicio registrada. Desafortunadamente, este enfoque es frágil porque el cliente del servicio depende completamente del motor de detección de servicios que se está ejecutando para encontrar e invocar un servicio.

Un enfoque más sólido es utilizar lo que se denomina balanceo de carga del lado del cliente. En este modelo, cuando un actor consumidor necesita invocar un servicio, se pondrá en contacto con el servicio de detección de servicios para todas las instancias de un servicio. El consumidor está solicitando y luego almacenar en caché los datos localmente en la máquina del consumidor del servicio.

Cada vez que un cliente desee llamar al servicio, el consumidor del servicio buscará la información de ubicación del servicio en la memoria caché. 

El almacenamiento en caché utilizará un algoritmo de balanceo de carga simple como el algoritmo de balanceo de carga "round-robin" para garantizar que las llamadas de servicio se distribuyan entre múltiples instancias de servicio.

Luego, el cliente se comunicará periódicamente con el servicio de descubrimiento de servicios y actualizará su caché de instancias de servicio. El caché del cliente finalmente es consistente, pero siempre existe el riesgo de que, entre el momento en que el cliente se pone en contacto con la instancia de detección de servicios para una actualización y se realicen las llamadas, las llamadas se dirijan a una instancia de servicio que no está en buen estado.

Si, durante el transcurso de la llamada a un servicio, la llamada de servicio falla, la memoria caché de detección de servicios local se invalida y el cliente de detección de servicios intentará actualizar sus entradas desde el agente de detección de servicios.

lunes, 17 de enero de 2022

Como tiene que ser el Service discovery en microservicios?


Ya vimos que no podemos utilizar un balanceador de carga tradicional para implementar el service discovery. Dado que la solución para un entorno de microservicios basado en la nube es usar un mecanismo de descubrimiento de servicios debe ser :

  • Altamente disponible: el descubrimiento de servicios debe ser compatible con un entorno de agrupación en clúster "caliente" donde las búsquedas de servicios se pueden compartir entre varios nodos en un clúster de descubrimiento de servicios. Si un nodo deja de estar disponible, otros nodos del clúster deberían poder reemplazarlo.
  • De igual a igual: cada nodo en el clúster de detección de servicios comparte el estado de una instancia de servicio.
  • Equilibrio de carga: el descubrimiento de servicios debe equilibrar dinámicamente la carga de solicitudes en todas las instancias de servicio para garantizar que las invocaciones de servicio se distribuyan en todas las instancias de servicio que gestiona. En muchos sentidos, el descubrimiento de servicios reemplaza a los balanceadores de carga más estáticos y administrados manualmente que se usaban en muchos de los primeras implementaciones de aplicaciones web.
  • Resiliente: el cliente del descubrimiento de servicios debe "almacenar en caché" la información del servicio localmente. El almacenamiento en caché local permite la degradación gradual del descubrimiento del servicio para que si el servicio de detección de servicios no está disponible, las aplicaciones aún pueden funcionar y ubicar los servicios en función de la información mantenida en su caché local.
  • Tolerante a fallas: el descubrimiento de servicios debe detectar cuándo una instancia de servicio no está en buen estado y eliminar la instancia de la lista de servicios disponibles que pueden tomar solicitudes de clientes. Debe detectar estas fallas con los servicios y tomar medidas sin intervención humana.

Podemos usar balanceadores de carga para implementar service discovery?


Hemos repasado los beneficios del descubrimiento de servicios, pero ¿cuál es el problema? Después de todo, ¿no podemos usar métodos probados y verdaderos como DNS (Servicio de nombres de dominio) o un balanceador de carga para ayudar a facilitar el descubrimiento de servicios? Analicemos por qué eso no funciona con una aplicación basada en microservicios, particularmente una que se ejecuta en la nube.

Siempre que tenga una aplicación que llama a recursos repartidos en varios servidores, necesita ubicar la ubicación física de esos recursos. En el mundo normal (no clud), esta resolución de ubicación de servicio a menudo se resolvía y se resuelve mediante una combinación de DNS y un balanceador de carga. 

Una aplicación si necesita invocar un servicio intenta invocar el servicio utilizando un nombre genérico junto con una ruta que representa de forma única el servicio que la aplicación intentaba invocar. El nombre DNS se resolvería en un balanceador de carga. 

El balanceador de carga, al recibir la solicitud del consumidor del servicio, ubica la entrada de la dirección física en una tabla de enrutamiento según la ruta a la que el usuario intentaba acceder. Esta entrada de la tabla de enrutamiento contiene una lista de uno o más servidores que alojan el servicio. Luego, el balanceador de carga elige uno de los servidores de la lista y reenvía la solicitud a ese servidor.

Cada instancia de un servicio se implementa en uno o más servidores de aplicaciones. La cantidad de estos servidores de aplicaciones a menudo era estática (por ejemplo, la cantidad de servidores de aplicaciones que alojaban un servicio no aumentaba ni disminuía) y persistente (por ejemplo, si fallaba un servidor que ejecutaba un servidor de aplicaciones, se restauraría al estado actual). mismo estado que tenía en el momento del bloqueo, y tendría la misma IP y configuración que tenía anteriormente).

Para lograr una forma de alta disponibilidad, un balanceador de carga secundario está inactivo y hace ping al balanceador de carga principal para ver si está activo. Si no está activo, el balanceador de carga secundario se activa, asumiendo la dirección IP del balanceador de carga principal y comenzando a atender las solicitudes.

Si bien este tipo de modelo funciona bien con aplicaciones que se ejecutan dentro de las cuatro paredes de un centro de datos corporativo y con una cantidad relativamente pequeña de servicios que se ejecutan en un grupo de servidores estáticos, no funciona bien para aplicaciones de microservicio basadas en la nube. Las razones para esto incluyen :

  • Punto único de falla: si bien el balanceador de carga puede tener una alta disponibilidad, es un punto único de falla para toda su infraestructura. Si el balanceador de carga se cae, todas las aplicaciones que dependen de él también se caen. Si bien puede hacer que un balanceador de carga esté altamente disponible, los balanceadores de carga tienden a ser cuellos de botella centralizados dentro de la infraestructura de la aplicación.
  • Escalabilidad horizontal limitada: al centralizar sus servicios en un solo grupo de balanceadores de carga, tiene una capacidad limitada para escalar horizontalmente su infraestructura de balanceo de carga en varios servidores. Muchos balanceadores de carga comerciales están limitados por dos cosas: su modelo de redundancia y los costos de licencia. La mayoría de los balanceadores de carga comerciales usan un modelo de intercambio en caliente para la redundancia, por lo que solo tiene un único servidor para manejar la carga, mientras que el balanceador de carga secundario está allí solo para la conmutación por error en el caso de una interrupción del balanceador de carga principal. Estás, en esencia, limitado por tu hardware. En segundo lugar, los balanceadores de carga comerciales también tienen modelos de licencia restrictivos orientados a una capacidad fija en lugar de un modelo más variable.
  • Gestionado estáticamente: la mayoría de los balanceadores de carga tradicionales no están diseñados para registrar y cancelar el registro de servicios rápidamente. Usan una base de datos centralizada para almacenar las rutas para las reglas y la única forma de agregar nuevas rutas es a menudo a través de la API (interfaz de programación de aplicaciones) propietaria del proveedor.
  • Complejo: debido a que un balanceador de carga actúa como un proxy para los servicios, las solicitudes de los consumidores de servicios deben tener sus solicitudes asignadas a los servicios físicos. Esta capa de traducción a menudo agregaba una capa de complejidad a su infraestructura de servicio porque las reglas de mapeo para el servicio deben definirse e implementarse a mano. En un escenario de balanceador de carga tradicional, este registro de nuevas instancias de servicio se realizaba a mano y no en el momento del inicio de una nueva instancia de servicio.

Estas cuatro razones no son una acusación general de los balanceadores de carga. Funcionan bien en un entorno corporativo donde el tamaño y la escala de la mayoría de las aplicaciones pueden manejarse a través de una infraestructura de red centralizada. Además, los balanceadores de carga aún tienen un papel que desempeñar en términos de centralizar la terminación SSL y administrar la seguridad del puerto del servicio. Un balanceador de carga puede bloquear el acceso al puerto de entrada (entrada) y salida (salida) a todos los servidores que se encuentran detrás de él. Este concepto de acceso mínimo a la red suele ser un componente crítico cuando se trata de cumplir con los requisitos de certificación estándar de la industria, como el cumplimiento de PCI (industria de tarjetas de pago).

Sin embargo, en la nube, donde tiene que lidiar con cantidades masivas de transacciones y redundancia, una pieza centralizada de infraestructura de red no funciona tan bien en última instancia porque no se escala de manera efectiva y no es rentable. 

sábado, 15 de enero de 2022

Deploying NGINX as an Api Gateway

 Me llego este mail con un libro gratuito y se los comparto: 

EBOOK

Image

Enterprises use NGINX load‑balancing features to complement existing hardware ADCs, or even to replace them, and while converting applications from monoliths to microservices. Among the many use cases for NGINX, the API gateway use case is becoming increasingly widespread, especially when consolidating load balancers and API gateways into a single solution.

We show you how to extend NGINX to manage API traffic in a comprehensive set of use cases. We start off with information about configuring the API gateway for single-service and microservices APIs, rewriting client requests, responding to errors, and authenticating users. Then you learn how to secure the gateway with rate limiting, access controls, limits on request methods and size, and other techniques. Finally, we explain how to publish gRPC services.

With NGINX for API management, you tap into the high performance, reliability, robust community support, and advanced functionality and professional support (for NGINX Plus customers) that NGINX is famous for.

In this eBook you will learn:

  • About using NGINX as a complement and replacement for existing API gateway and API management approaches
  • How to extend the configuration of an existing NGINX instance to also manage API traffic
  • About a range of safeguards for protecting and securing backend API services in production
  • How to deploy NGINX as an API gateway for gRPC services