try? convirtió una interrupción en "todo al día"
Justo después de agregar inyección de errores a mis mocks, fui a revisar el único lugar al que el nuevo seam no llegaba. Resultó estar escondiendo el peor bug de la app.
El problema
Deep Cut Atlas arma su feed obteniendo el catálogo de cada artista de la biblioteca, unos pocos artistas a la vez:
let tasks = chunk.map { artist in
Task { () -> [LibraryAlbum] in
(try? await self.service.fetchCatalogReleases(for: artist)) ?? []
}
}
Ese try? ... ?? [] parecía inofensivo. Un artista con fallas intermitentes no debería tumbar un barrido de 200 artistas, ¿no? Resiliencia por artista. Hasta tenía un comentario explicando que el chunking era una protección de rate-limit.
Pero si se sigue el array vacío río abajo: el diff lo trata como “este artista no tiene lanzamientos”. El resultado queda cacheado con un TTL de 24 horas. Entonces, si la red se cae a mitad de un refresh: cada artista “no tiene lanzamientos”, el diff queda vacío, el feed vacío se cachea sobre el bueno, y la interfaz reporta alegremente “You’re all caught up!” durante el día siguiente.
Una interrupción no parecía una interrupción. Parecía un éxito sin datos, que es exactamente lo que hace try?: vuelve el fallo indistinguible de la ausencia.
La solución
Ahora cada fetch devuelve un Result en lugar de tragarse el error, los fallos se registran por artista, y el barrido reporta si estuvo completo:
private struct CatalogSweep {
let albums: [LibraryAlbum]
let isComplete: Bool
}
De ahí salen tres comportamientos. Si todos los artistas fallaron, lanzar un error, el camino de error existente lo maneja: una carga en frío muestra el fallo, un refresh en segundo plano sigue mostrando el feed cacheado. Si algunos fallaron, mostrar el feed parcial (fresco pero parcial le gana a nada) pero no cachearlo, el feed en pantalla se autocorrige en el siguiente refresh, mientras que uno cacheado sería la base durante un día. Si ninguno falló, cachear como antes.
El mock necesitó un seam más para probar esto: inyección de fallos por artista (failingArtistNames: Set<String>), porque la inyección a nivel de método de la ronda anterior solo podía hacer fallar a todos los artistas a la vez. Cuatro pruebas fijan la matriz: fallo total y parcial, en carga en frío y en refresh de segundo plano.
Lecciones
try? en un pipeline de datos significa que el fallo se convierte en dato. Si ese dato alimenta un caché, se construyó un mecanismo para persistir interrupciones como si fueran hechos.
Resiliencia y visibilidad son decisiones separadas. Mantener el barrido con vida más allá de un artista con fallas está bien; fingir que el artista no tenía nada no lo está. Result da ambas cosas.
Y escribir pruebas de fallo es cómo encontré esto. El seam de inyección de errores no solo cubrió las ramas existentes, hizo que notara el único error que nunca llegaba a ninguna rama.
Lecturas relacionadas
Una sección de tracklist, y por qué tomó 30 minutos
Un método de protocolo, un enum de estado reutilizado, una convención de enrutamiento que se mantuvo, y un límite de lint que forzó una división que valía la pena hacer de todos modos.
Recuperé un fix guardado en el stash, y una reproducción en vivo en mi teléfono me mostró un error distinto
Un stash apply limpio, una regla de SwiftLint que compara sintaxis en vez de rastrear consumo, y un bloqueo en tres pestañas que no era el error ya arreglado.
Un reporte de bug de filtro que se convirtió en tres issues separados
El filtro reportado funcionaba bien: el problema real era otro filtro, deliberadamente independiente, y la solución fue una tercera opción no considerada.
También te podría ser útil
Proton Mail
Correo electrónico cifrado de extremo a extremo, con arquitectura de acceso cero.
Como socio de Proton, obtengo ingresos por las compras que califican de los servicios de privacidad y seguridad de Proton (Pass, Mail, VPN, Drive).
Más informaciónProton Pass
Gestor de contraseñas centrado en la privacidad, del equipo detrás de Proton Mail.
Como socio de Proton, obtengo ingresos por las compras que califican de los servicios de privacidad y seguridad de Proton (Pass, Mail, VPN, Drive).
Más informaciónAdGuard para iOS
Bloqueo de anuncios y rastreadores en todo el sistema en iOS, sin necesidad de un servidor DNS aparte.
Como afiliado de AdGuard, obtengo ingresos por las compras que califican.
Más información