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:
- Hay un proceso que de forma continua lee esa cola y usa un cliente UDP para reportarlas.
- Cuando uno quiere reportar una métrica uno invoca una función que empuja un valor a esa cola.
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()
.queue >>= (queue => {
clientimplicit val q = queue
for {
<- client.listen // uses implicit 'q'
z <- RIO.foreach(messages)(d => Task(Counter("clientbar", d, 1.0, Seq.empty[Tag])))
opt <- RIO.collectAll(opt.map(m => client.send(q)(m)))
_ } yield z
})
}
Hay varios detalles:
- Los usuarios de la librería tienen que llamar
.listen
para inicializar el proceso que va a extraer valores de la cola. - Después de eso, cada vez que quieran enviar una métrica tienen que, además, proveer la referencia a la cola.
Esto conlleva varios problemas:
- Un usuario puede olvidar llamar
.listen
perfectamente. Nada en la librería evitaría que esto pasase. - Un usuario puede enviar una métrica con una referencia a una cola distinta a la que fué usada al llamar a
.listen
. Una vez más, nada en la librería evitaría que esto sucediera. - Por último, tal vez no sea evidente en el ejemplo anterior, pero en programas más complicados, dónde los pasos de inicialización están muy alejados de los usos, puede resultar incómodo usar este API. Habría que propagar la referencia a la cola desde dónde se inicializó.
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()
.use { client =>
createClientfor {
<- RIO.foreach(messages)(d => Task(Counter("clientbar", d, 1.0, Seq.empty[Tag])))
opt <- 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(
: Long,
bufferSize: Long,
timeout: Int,
queueCapacity: Option[String],
host: Option[Int]
port): ZManaged[ClientEnv, Throwable, Client] =
.make {
ZManagedfor {
<- ZQueue.bounded[Metric](queueCapacity)
queue = new Client(bufferSize, timeout, host, port)(queue)
client <- client.listen
fiber } 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.