Cuando el fichero ya no cabe en la petición
Entregar un fichero que se genera al pedirlo funciona mientras el paquete sea pequeño: se construye sobre la respuesta y no se guarda nada. Esa entrada dejó escrito cuándo deja de valer — “si el paquete deja de ser pequeño o aparecen timeouts”—, y este es ese caso.
Si el fichero no se puede construir dentro del tiempo de una petición, o tiene que seguir existiendo mañana, deja de ser una respuesta y pasa a ser un objeto con estado.
Dos cosas cambian a la vez, y conviene no confundirlas: el tamaño obliga a construirlo fuera de la petición; la disponibilidad obliga a guardarlo. Si solo pasara lo primero, bastaría con construirlo y servirlo una vez.
La máquina de estados
Una fila por petición: PENDING → RUNNING → READY, o FAILED, y EXPIRED cuando caduca. La
petición del usuario solo crea la fila y devuelve su id; el trabajo lo hace
una tarea programada que recoge las pendientes.
Tres detalles que se pagan caros si faltan:
- Reclamar antes de trabajar. El paso a
RUNNINGes condicional —“solo si sigue enPENDING”—, y ahí muere el problema de dos pasadas solapadas. - Rearmar al arrancar. Lo que un reinicio dejó en
RUNNINGvuelve aPENDING, o se queda colgado para siempre. - Una en curso bloquea otra. Pedir dos veces el mismo paquete no es dos trabajos, es el mismo dos veces.
No hace falta una cola para esto. Una cola aporta reintentos, prioridades y concurrencia entre procesos; si son unas pocas ejecuciones al año y un solo proceso, traerla es más pieza que problema.
Leer sin bloquear, escribir sin memoria
La construcción tiene dos mitades con reglas opuestas.
Leer quiere una foto coherente: una transacción REPEATABLE READ para que lo que se escriba a
mitad no deje el paquete inconsistente. Pero una transacción abierta mientras suben gigas es una
transacción que se queda abierta demasiado tiempo, así que dentro solo se leen filas y claves de
almacenamiento; los ficheros se leen después, fuera.
Cuidado con el tiempo por defecto: en Prisma, una transacción interactiva corta a los 5 segundos si no le pasas
timeout. Es el error que solo aparece con el primer cliente grande.
Escribir quiere no acumular: el empaquetador escribe sobre un stream que va directo al
almacenamiento, que lo sube por partes. Entre los dos se cuela un Transform que calcula el
SHA-256 y el tamaño al vuelo — leer el fichero otra vez para sumarlo sería doblar el trabajo.
Ahí hay una trampa fácil: si una mitad falla hay que destruir el stream para que la otra se entere, y el stream destruido necesita un manejador de error, o el proceso entero se cae. Sin eso, una subida que falla deja al empaquetador esperando a alguien que ya no existe.
La descarga, días después
Ya no es la respuesta a “generar”, sino una petición aparte, y eso cambia tres cosas:
| En la petición | En segundo plano | |
|---|---|---|
| Token | De un solo uso | Vale varias veces, vida corta |
Range |
No aplica | Necesario |
| Suma | No hay nada que comparar | Se entrega y va como ETag |
El token deja de ser de un solo uso justamente por el tamaño: una descarga de gigas se corta, y el
gestor de descargas reanuda con Range sobre la misma URL. Un token que se gasta en el primer
byte convierte cada corte en empezar de cero.
Caducar es parte del diseño
Un paquete con todos los datos de alguien, en reposo, es el peor objeto del sistema. Existe porque
tiene que seguir disponible un tiempo, y deja de existir en cuanto ese tiempo pasa: un cron borra el
fichero y marca la fila EXPIRED.
La fila se queda, sin fichero: es la constancia de que el paquete se generó y se entregó. Borrar la fila con el objeto sería borrar la prueba junto con el dato.
Qué probar
- Dos pasadas del cron a la vez → un solo paquete.
- Un fallo a mitad → la fila queda
FAILEDy el objeto a medias no se queda en el almacenamiento. - La suma que devuelve el listado → coincide con la del fichero descargado.
- Un
Range→ devuelve exactamente el tramo pedido, y uno imposible responde 416.