264 lines
10 KiB
Markdown
264 lines
10 KiB
Markdown
# NATS APP
|
|
|
|
Lectura de datos de sensores en un dispositivo IoT. Prueba técnica para optar
|
|
por el puesto de programador Go.
|
|
|
|
## TL:DR
|
|
|
|
- make lazy-start
|
|
- abre terminal y escribe: `nats sub sensors.data.*`
|
|
|
|
## Requisitos previos
|
|
|
|
- Docker
|
|
- NATS CLI
|
|
- Make, si prefieres la comodidad de usar Makefile
|
|
|
|
## Comandos
|
|
|
|
### Registrar un sensor
|
|
|
|
- Campos obligatorios: `sensor_id` y `sensor_type`.
|
|
- Campos opcionales: `sampling`, `thresholdabove` y `thresholdbelow`.
|
|
|
|
`nats req sensors.register '{
|
|
"sensor_id": "sensor-001",
|
|
"sensor_type": "temperature",
|
|
"sampling": 3600,
|
|
"thresoldabove": 50.0,
|
|
"thresoldbelow": -10.0
|
|
}'`
|
|
|
|
### Actualizar configuración sensor
|
|
|
|
- Campo obligatorio: `sensor_id` y la presencia de al menos un parámetro.
|
|
|
|
`nats req sensors.update '{
|
|
"sensor_id": "sensor-001",
|
|
"sensor_type": "temperature",
|
|
"sampling": 10,
|
|
"thresoldabove": 100.0,
|
|
"thresoldbelow": -15.0
|
|
}'`
|
|
|
|
### Obtener información de un sensor
|
|
|
|
- Campo obligatorio: `sensor_id`.
|
|
|
|
`nats req sensors.get '{
|
|
"sensor_id": "sensor-001"
|
|
}'`
|
|
|
|
### Obtener valores de un sensor
|
|
|
|
- Campo obligatorio: `sensor_id`.
|
|
- Campos opcionales: `from` y `to` en formato RFC3339. Si no se especifican,
|
|
se toman los últimos 7 días.
|
|
|
|
`nats req sensors.values.get '{
|
|
"sensor_id": "sensor-001",
|
|
"from": "2025-10-03T00:00:00Z",
|
|
"to": "2025-10-10T23:59:59Z"
|
|
}'`
|
|
|
|
### Obtener listado de sensores
|
|
|
|
No hay _payload_, pero hay que poner comillas dobles o si no se queda esperando
|
|
una entrada de datos.
|
|
|
|
`nats req sensors.list ""`
|
|
|
|
### Suscribirse a un sensor
|
|
|
|
`nats sub sensors.data.sensor-001`
|
|
|
|
### Suscribirse a todos los sensores
|
|
|
|
`nats sub sensors.data.*`
|
|
|
|
## Consideraciones
|
|
|
|
Hay partes de códigos que son _snippets_ extraídos de una librería de autoría
|
|
propia. [Repositorio GitHub](https://github.com/zepyrshut/gopher-toolbox). De
|
|
las cuales son:
|
|
|
|
- El _logger_ usando la _stdlib log/slog_.
|
|
- La conexión con la base de datos, usando el controlador [pgx](https://github.com/jackc/pgx).
|
|
|
|
|
|
## Bitácora
|
|
|
|
### Quickstart y toma de contacto con NATS
|
|
|
|
Lo primero que he hecho es un _quickstart_ del proyecto, con lo que siempre o
|
|
casi siempre pongo en mis experimentos. Y lo siguiente, en lugar de empezar a
|
|
construir el proyecto como loco he tratado de entender cómo funciona NATS. Al
|
|
final ha sido muy sencillo, siguiendo esos pasos:
|
|
|
|
1. Levantar el servidor NATS en Docker
|
|
2. Instalar el CLI de NATS
|
|
2. Abrir un puñado de terminales, y en un par de ellas escribir: `nats sub "hello"`
|
|
lo cual significa que se está suscribiendo al canal `hello`. Y en otra escribir:
|
|
`nats pub "hello" "Hola mundo!"`, lo cual significa que está escribiendo el
|
|
mensaje `Hola mundo!` en el canal `hello`.
|
|
|
|

|
|
|
|
### Creación de la aplicación
|
|
|
|
La parte fácil es hacer todo el _boilerplate_, hecho en solo _commit_, que no es
|
|
lo ideal pero lo dicho, es puro relleno, mucho código ya viene escrito de la
|
|
librería `gopher-toolbox`. En este punto el proyecto compila pero hay error en
|
|
tiempo de ejecución por la falta de implementaciones en el repositorio.
|
|
|
|
### Empezando por el repositorio
|
|
|
|
El almacenamiento de los datos se ha optado por el uso de `TimescaleDB`, una
|
|
extensión de `PostgreSQL`, el motivo es porque ya he trabajado mucho con ese
|
|
motor y tengo los _drivers_ ya escritos. Tengo entendido que está optimizado
|
|
para trabajar con grandes cantidades de datos de series temporales, lo que viene
|
|
siendo valores de sensores por ejemplo.
|
|
|
|
Por otro lado también hay un sistema de caché muy rudimentario, en memoria que
|
|
es un mapa de valores.
|
|
|
|
Para el registro de valores y mantener ambos se ha usado el patrón decorador que
|
|
bajo un mismo _struct_ se incluye las dos implementaciones y registra cambios en
|
|
ambas partes. Desde la capa servicios sólo tiene que llamar al decorador sin
|
|
saber los detalles de la implementación.
|
|
|
|
### Continuamos con los servicios
|
|
|
|
Con el repositorio sin implementar, se puede realizar los servicios. En ese
|
|
proyecto ha quedado muy básico, sirviendo solamente de enlace entre los
|
|
controladores y el repositorio.
|
|
|
|
Cuando se creó el _broker_ de NATS, inicialmente fue para crear una interfaz de
|
|
mensajería donde se pudiese manejar _websockets_, _SSE_ y otras mensajerías pero
|
|
se consideró que había otras prioridades. Así se quedó.
|
|
|
|
### Y finalmente los controladores
|
|
|
|
Ahí tuve muchas dudas con el entendimiento de NATS, estaba muy arraigado en el
|
|
patrón REST, y cambiar la mentalidad costó un poco, al final mirando un poco la
|
|
documentación me quedé con los conceptos clave:
|
|
|
|
1. Está basado en asuntos, los canales se crean de forma jerárquica.
|
|
2. Cuando se hace un _subscribe_, se suma a un canal del asunto dado.
|
|
3. Para escribir en el canal, hay que hacer el _publish_.
|
|
4. Finalmente para solicitar un recurso, está el _request_.
|
|
|
|
Esto es todo, entonces los controladores de la entidad _sensors_ están
|
|
constituidos por una serie de _endpoints_ haciendo las acciones que se solicita.
|
|
|
|
### El simulador
|
|
|
|
Basada en _gorutinas_ y canales, cuando se inicia el simulador, se crea un canal
|
|
para detener simuladores que están en ejecución para su actualización o
|
|
detención.
|
|
|
|
Cuando se registra un nuevo sensor, está la función SimulateSensor, que se
|
|
inicia como una _gorutina_ y usa el `SamplingInterval` para el canal `ticker`,
|
|
así llamar a `generateData` cada vez que toque.
|
|
|
|
Una vez que el dato está generado se hace una publicación al asunto _sensor.data_,
|
|
que al mismo tiempo, el _handler_ registerData lo captura al estar registrado
|
|
al mismo asunto _sensor.data_.
|
|
|
|
## Pruebas
|
|
|
|
La realización de pruebas unitarias de lo que son los controladores de NATS me
|
|
han sido imposible hacerlas en condiciones, podría haber usado Claude pero es
|
|
que no daba pie con bola y no entendía nada, así que por la máxima transparencia
|
|
he optado por no incorporarlas.
|
|
|
|
Las pruebas más interesantes son las de reglas de negocio y validación, lo que
|
|
viene a ser los servicios y dominio.
|
|
|
|
## Generadores y otras librerías
|
|
|
|
Existen generadores de código para Golang, de hecho, se fomenta su desarrollo,
|
|
hay un artículo interesante de Rob Pike [hablando sobre ello](https://go.dev/blog/generate).
|
|
Muchas de las herramientas son muy interesantes usarlas ya que acelera mucho la
|
|
generación de código repetitivo. Da mas confianza usar esas herramientas que la
|
|
IA.
|
|
|
|
Para las consultas SQL con seguridad de tipos, existe la herramienta [sqlc](https://sqlc.dev/).
|
|
|
|
Para ese proyecto sólo se ha usado [GoMock](https://github.com/uber-go/mock),
|
|
mantenida por Uber y sirve para usar la interfaz `Repository` sin usar una
|
|
base de datos real.
|
|
|
|
Por otro lado, hubiese sido interesante incorporar [ginkgo](https://onsi.github.io/ginkgo/),
|
|
es un marco de trabajo de pruebas unitarias usando un lenguaje de dominio
|
|
específico (DSL).
|
|
|
|
> En lugar de escribir una prueba así: `Test_Validate(t *testing.T) { ... }`
|
|
> se puede hacer de la siguiente manera: `var _ Describe("Models", func () { ... })`,
|
|
> y dentro del cuerpo se describen las pruebas a realizar.
|
|
|
|
No se ha incorporado porque hay que instalar la herramienta que ejecutan las
|
|
pruebas, y no quería correr el riesgo de que no funcionase en otro equipo o no
|
|
diesen los resultados esperados. Que se podría haber usado un contenedor Docker,
|
|
sí, pero la prueba no consiste en eso.
|
|
|
|
También se ha planteado incorporar la librería _testify_, descartado porque para
|
|
comprobar si existe el error y algunas comparaciones no era necesario meter una
|
|
dependencia más.
|
|
|
|
## Diagrama
|
|
|
|

|
|
|
|
El diagrama explica básicamente la estrucutra del proyecto en términos generales,
|
|
se demuestra que el dominio sensors solo se comunica al exterior mediante el
|
|
NATS y el _logger_.
|
|
|
|
Amarillo: exterior.
|
|
Morado: infraestructura.
|
|
Verde: dominio
|
|
Blanco: simulador, los _handlers_ está conectado sólo para llamar a la
|
|
gorutina, pero en la realidad debería ir independiente.
|
|
|
|
## Conclusión y cierre
|
|
|
|
Interesante reto donde realmente lo que más me ha costado son la realización
|
|
de pruebas unitarias, hay veces que me cuesta coger el concepto. Tengo la teoría
|
|
muy clara, pero a la hora de la verdad se me complica un poco las cosas. Además
|
|
que _mockear_ el sistema de mensajería debe tener su especial complejidad.
|
|
También ha sido algo complejo entender la concurrencia y los canales, no es algo
|
|
que haya trabajado en profundidad pero sí que se le ha puesto bastante empeño,
|
|
cariño y nivel de detalle.
|
|
|
|
He puesto mucho en valor la arquitectura limpia con un toque personal, no es DDD
|
|
puro ya que hay elementos que no deberían estar en dominio, pero en el proyecto
|
|
que estoy trabajando ahora mismo se está diseñando de la misma manera y está
|
|
funcionando muy bien.
|
|
|
|
También se ha evitado todo lo posible el uso de LLMs para la generación de
|
|
código, y su uso ha sido para la toma de decisiones arquitectónicas, discusión y
|
|
lectura rápida sobre los distintos funcionamientos de algunas librerías. En más
|
|
de una ocasión he cuestionado las respuestas que da, teniendo que verificar con
|
|
la documentación oficial. Pongo en valor mi capacidad para aprovechar la IA de
|
|
la mejor forma posible, verificando la información, además recalco que justo el
|
|
proyecto actual es una migración de un código en PHP completamente hecho con IA,
|
|
y se puede ver patrones y errores comunes que comete.
|
|
|
|
Soy consciente de que hay margen de mejora, por ejemplo con los tests o con la
|
|
documentación, se ha puesto especial esfuerzo y atención a que los nombres de
|
|
las funciones, variables, métodos, estructuras y paquetes sean lo más
|
|
autodescriptivos posibles. Se han puesto algunos comentarios. También hay
|
|
esfuerzo por permitir ejecutar el proyecto por primera vez con la mínima
|
|
intervención.
|
|
|
|
Un problema interesante que tuve que resolver, que como el sensor puede mandar
|
|
un valor ausente, el tipo `float64` al hacer el `unmarshal` se establece a 0.0,
|
|
con lo que se puede considerar válido, con lo cual su solución fue el uso de
|
|
puntero, si se descubre que es `nil` se considera no válido.
|
|
|
|
Digamos que este proyecto resuelve el problema que se propone, un sistema que
|
|
permite registrar y actualziar un sensor. Se puede ver su estado y los datos que
|
|
se recogen (simulados) se guardan en una base de datos.
|
|
|
|
Espero que el proyecto sea de vuestro agrado y podamos tener una siguiente
|
|
reunión. |