Algunos errores se arreglan solos
Un servicio en la nube responde a cada llamada, y a veces la respuesta es un error. Un código de error te dice de qué clase es.
Un bucket de S3 que no existe seguirá sin existir si vuelves a preguntar. Una tabla de DynamoDB ocupada puede responder un segundo después. Esta lección enseña a distinguir los dos casos y a reintentar el correcto.
Al final de esta lección
Nota
Puedo distinguir un error que se reintenta de uno que se arregla.
Palabras clave
| Palabra | Qué significa | Ejemplo |
|---|---|---|
| Código de error | un nombre corto de lo que salió mal | NoSuchKey |
| Código de estado | un número que clasifica una respuesta en una familia | 404 |
| Limitación | un servicio que rechaza llamadas porque enviaste demasiadas | throttling |
| Reintento | enviar otra vez la misma llamada | |
| Espera creciente | esperar más antes de cada reintento | backoff |
| Jitter | una parte aleatoria de la espera, para que los clientes no se muevan a la vez |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Un error tiene un código y un estado
Cuando una llamada falla, boto3 te da un error. Lleva un código de estado, como 404, y un código de error, como NoSuchKey.
Comprueba el código de error y el estado. No compruebes el mensaje: su redacción puede cambiar entre servicios, y la del emulador difiere de la real en algunos casos.
Ejemplo resuelto: errores leídos de respuestas capturadas
| Llamada | Code | Estado | ¿Reintentar? |
|---|---|---|---|
| get_object, la clave no existe | NoSuchKey | 404 | No, arregla la clave |
| head_object, la clave no existe | 404 | 404 | No, no hay cuerpo que leer |
| list_objects_v2, el bucket no existe | NoSuchBucket | 404 | No, crea el bucket |
| put_item, condición falsa | ConditionalCheckFailedException | 400 | No, la respuesta es la respuesta |
| query, nombre de clave equivocado | ValidationException | 400 | No, arregla la llamada |
| send_message, la cola no existe | AWS.SimpleQueueService.NonExistentQueue | 400 | No, crea la cola |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Run: lee el código y el estado
answers = [
{"error": {"Code": "NoSuchKey", "Message": "Falta.", "HTTPStatusCode": 404}},
{"error": {"Code": "ConditionalCheckFailedException", "Message": "No.", "HTTPStatusCode": 400}},
{"error": {"Code": "InternalServerError", "Message": "Vaya.", "HTTPStatusCode": 500}},
{"Items": []},
]
for answer in answers:
if "error" not in answer:
print("funcionó")
else:
error = answer["error"]
print(error["Code"], error["HTTPStatusCode"])
Reintenta lo que puede funcionar la próxima vez
Algunos errores dicen «ahora no». Un error de limitación significa que enviaste demasiadas llamadas. Un estado de 500 o más significa que el servicio tuvo un fallo. Los dos pueden pasar si esperas.
Otros errores dicen «esta llamada no». Tu llamada estaba mal, así que la misma llamada falla siempre. Arréglala en lugar de reintentar.
Un error frecuente
Advertencia
Mucha gente reintenta todos los errores por si acaso. Una clave equivocada falla entonces cinco veces y se pierde la espera. Reintenta solo la limitación y los fallos del servidor. Arregla todos los demás errores, porque la misma llamada recibe la misma respuesta.
Run: separa reintentar y arreglar
# Los códigos de limitación son los documentados, no capturados aquí.
THROTTLING = ["ThrottlingException", "ProvisionedThroughputExceededException", "SlowDown"]
def worth_retrying(error):
if error["HTTPStatusCode"] >= 500:
return True
return error["Code"] in THROTTLING
errors = [
{"Code": "NoSuchKey", "HTTPStatusCode": 404},
{"Code": "SlowDown", "HTTPStatusCode": 503},
{"Code": "ThrottlingException", "HTTPStatusCode": 400},
{"Code": "ValidationException", "HTTPStatusCode": 400},
]
for error in errors:
print(error["Code"], worth_retrying(error))
# Añade "RequestLimitExceeded" a THROTTLING y pruébalo.
La espera crece en cada intento
Si un servicio está ocupado, un reintento un milisegundo después lo ocupa más. Así que espera, y espera más después de cada fallo. Duplicar la espera es lo habitual. Pon un tope para que la espera nunca pase de un límite.
Ahora añade jitter: espera una parte aleatoria de ese tiempo. Sin él, cien clientes se despiertan a la vez y vuelven a fallar a la vez.
Ejemplo resuelto: el techo de cada espera
| Intento | Duplicado | Con tope de 8 | Espera si pick es 0,5 |
|---|---|---|---|
| 1 | 0,5 segundos | 0,5 | 0,25 |
| 2 | 1 segundo | 1 | 0,5 |
| 3 | 2 segundos | 2 | 1 |
| 4 | 4 segundos | 4 | 2 |
| 5 | 8 segundos | 8 | 4 |
| 6 | 16 segundos | 8 | 4 |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Run: una espera que se puede probar
# pick es un número de 0 a 1. Un cliente real pasa random.random().
# Una prueba pasa un número fijo, así que la respuesta nunca cambia.
def backoff(attempt, base, cap, pick):
ceiling = min(cap, base * 2 ** (attempt - 1))
return ceiling * pick
for attempt in range(1, 7):
print(attempt, backoff(attempt, 0.5, 8, 1.0), backoff(attempt, 0.5, 8, 0.5))
# Cambia el tope de 8 a 2. ¿Qué intentos cambian?