Miguel Vilá

Un pequeño refactor

Nov 19 2020

Hace poco hice un Pull Request a una librería de métricas en Scala, con el objetivo de mejorar el API expuesto a usuarios. Resumiendo la cosa zio-metrics es una pequeña librería para publicar métricas con ZIO. La estamos usando para reportar una métrica a la medida pero al usarla, en su momento, me dí cuenta que tenía unos detalles de diseño que no me gustaban.

La librería funciona con una cola de métricas. Esta cola de métricas es muestreada de forma asíncrona para reportarlas:

Un ejemplo, tomado de los docs viejos, de como se usaba antes:

val program = {
    val messages = List(1.0, 2.2, 3.4, 4.6, 5.1, 6.0, 7.9)
    val client   = Client()
    client.queue >>= (queue => {
      implicit val q = queue
      for {
        z <- client.listen          // uses implicit 'q'
        opt <- RIO.foreach(messages)(d => Task(Counter("clientbar", d, 1.0, Seq.empty[Tag])))
        _   <- RIO.collectAll(opt.map(m => client.send(q)(m)))
      } yield z
    })
}

Hay varios detalles:

Esto conlleva varios problemas:

En pocas palabras la cola es un detalle de implementación que el API está exponiendo.

Debido a eso, me tomé el “atrevimiento” de abrir un issue. Lo discutí con el autor de la librería y después abrí un Pull request para intentar arreglarlo.

El resultado final implica que ahora ningún cliente tiene que pasar ninguna referencia a ninguna cola. El código anterior se transformó en:

val program = {
    val messages = List(1.0, 2.2, 3.4, 4.6, 5.1, 6.0, 7.9)
    val createClient = Client()
    createClient.use { client =>
      for {
        opt <- RIO.foreach(messages)(d => Task(Counter("clientbar", d, 1.0, Seq.empty[Tag])))
        _   <- RIO.collectAll(opt.map(m => client.sendM(true)(m)))
      } yield ()
    }
}

La diferencia principal es que ahora los constructores de los clientes retornan un ZManaged que encapsula la creación de la cola, y la inicialización del proceso que escucha la cola:

  def apply(
    bufferSize: Long,
    timeout: Long,
    queueCapacity: Int,
    host: Option[String],
    port: Option[Int]
  ): ZManaged[ClientEnv, Throwable, Client] =
    ZManaged.make {
      for {
        queue  <- ZQueue.bounded[Metric](queueCapacity)
        client = new Client(bufferSize, timeout, host, port)(queue)
        fiber  <- client.listen
      } yield (client, fiber)
    } { case (client, fiber) => client.queue.shutdown *> fiber.join.orDie }
      .map(_._1)

A partir de ahí las instancias de Client contarán con una referencia fija y privada a una cola. Además serán libres de empujar mensajes a ella cuando requieran reportar una métrica, sin tener que obligar a los invocadores a que provean la cola correcta.

Eso era todo, la lección principal es que uno no se tiene que aguantar APIs incómodos y que siempre hay forma de mejorarlos.

Gracias a toxicafunk por permitirme contribuir esto y asistirme en el PR.