El mòdul 6 va acabar amb un diagnòstic incòmode. La capa de dades de MercadoFresco està ben repartida —Aurora per a comandes i estoc, DynamoDB per a cistelles i sessions, Redshift per als informes de la Sara, ElastiCache per al catàleg—, però la petició que confirma una comanda continua fent vuit coses seguides abans de respondre. La Marta ho va mesurar al pic del divendres: 2.900 ms de mediana, 9.100 ms al percentil 95, i un 0,4 % de comandes que fallen després d'haver cobrat perquè l'ERP del magatzem va retornar un 503.

Amazon SQS (Simple Queue Service) trenca aquesta cadena. És una cua de missatges gestionada: un component escriu un missatge, un altre el llegeix quan pot, i cap dels dos no necessita que l'altre estigui viu en aquell instant. És el servei més antic d'AWS i també el més avorrit en el millor sentit: sense servidors, sense versions, sense capacitat que calgui dimensionar, i escala de zero a milions de missatges per segon sense que ningú toqui res. Aquí el Luis converteix les vuit crides encadenades en una confirmació de 400 ms més set tasques que passen després, i aprèn pel camí el concepte que més incidents causa en producció: el temps d'espera de visibilitat.

Avís de cost. SQS cobra per petició a l'API: 1 milió gratis al mes de manera permanent i 0,40 USD per milió addicional en cues estàndard (0,50 en FIFO). Una cua buida no costa res, però un consumidor amb sondeig curt pot generar centenars de milions de peticions buides al mes. Al final tens les ordres de neteja. Totes les dades són fictícies.

Contingut

  1. Síncron davant d'asíncron: què hi guanya MercadoFresco
  2. Anatomia d'una cua i el model de sondeig
  3. Cues estàndard i cues FIFO
  4. Quina cua fa servir cada tasca de MercadoFresco
  5. Cicle de vida d'un missatge
  6. El temps d'espera de visibilitat, en detall
  7. Sondeig curt i sondeig llarg
  8. Retenció, retard, temporitzadors i càrregues grans
  9. Cues de missatges fallits i redrive
  10. Crear les cues per CLI, política de cua i xifratge
  11. Productor i consumidor en Python
  12. Consumir des de Lambda: mapatge d'origen d'esdeveniments
  13. Mètriques, alarmes i autoescalat per profunditat de cua
  14. Cost real i neteja
  15. Errors habituals i consells
  16. Exercicis
  17. Conclusió

Síncron davant d'asíncron: què hi guanya MercadoFresco

Una crida síncrona és aquella en què qui truca espera la resposta abans de continuar. No té res de dolent mentre es faci servir per al que toca: obtenir una dada que es necessita ara per poder respondre. El problema apareix en encadenar crides síncrones a coses que no es necessiten ara. Aquestes són les vuit tasques de MercadoFresco, mesurades al pic del divendres:

# Tasca Destinació p50 p95 El client necessita el resultat?
1 Cobrar la targeta Passarel·la externa 240 ms 3.100 ms
2 Escriure la comanda Aurora 38 ms 60 ms
3 Buidar la cistella DynamoDB 11 ms 22 ms No
4 Invalidar la memòria cau ElastiCache 4 ms 9 ms No
5 Avisar l'ERP del magatzem HTTP del soci 1.180 ms 6.500 ms No
6 Correu de confirmació Proveïdor SMTP 820 ms 4.200 ms No
7 Notificar el repartidor API del soci 410 ms 2.900 ms No
8 Publicar esdeveniment d'analítica Firehose 190 ms 700 ms No

Només les dues primeres són imprescindibles per dir «la teva comanda està confirmada». Les sis restants són conseqüències de la comanda, no requisits, i tanmateix el client les paga en temps d'espera i en risc. La suma seqüencial dona 2.893 ms de mediana, però el greu és la fragilitat multiplicativa: si cada tasca té un 99,9 % de disponibilitat, la cadena de vuit té un 99,2 %. Amb 900 comandes per hora al pic, són set comandes trencades per hora, totes ja cobrades.

flowchart TD
    C[Client prem Confirmar comanda] --> A[Servidor de la botiga]
    A --> P1[1. Passarella de pagament 240 ms]
    P1 --> P2[2. Aurora INSERT 38 ms]
    P2 --> P3[3. DynamoDB buidar cistella 11 ms]
    P3 --> P4[4. ElastiCache invalidar 4 ms]
    P4 --> P5[5. ERP del magatzem 1.180 ms]
    P5 --> P6[6. Correu de confirmacio 820 ms]
    P6 --> P7[7. API del repartidor 410 ms]
    P7 --> P8[8. Esdeveniment analitic 190 ms]
    P8 --> R[Resposta al client 2.893 ms p50]
    P5 -. ERP retorna 503 .-> X[Comanda cobrada i perduda]
    style X fill:#f8d7da,stroke:#c00
    style R fill:#fff3cd

Una crida asíncrona trenca l'encadenament: la botiga deixa constància que hi ha feina pendent i respon. La feina es fa després, per un altre procés, amb els seus propis reintents.

flowchart TD
    C[Client prem Confirmar comanda] --> A[Servidor de la botiga]
    A --> P1[1. Passarella de pagament 240 ms]
    P1 --> P2[2. Aurora INSERT 38 ms]
    P2 --> Q[(cola-mercadofresco-pedidos<br/>SendMessage 12 ms)]
    Q --> R[Resposta al client ~400 ms p95]
    Q --> W1[Consumidor magatzem]
    Q --> W2[Consumidor correu]
    Q --> W3[Consumidor analitica]
    W1 --> ERP[ERP del magatzem]
    W2 --> SMTP[Proveidor de correu]
    W3 --> FH[Firehose i S3]
    ERP -. 503 .-> RT[Reintent automatic:<br/>el missatge segueix a la cua]
    style R fill:#d4edda
    style RT fill:#fff3cd

El que s'hi guanya no és només velocitat: la fallada de l'ERP deixa de ser la fallada de la comanda. El missatge segueix a la cua, el consumidor el reintenta, i si l'ERP triga dues hores a tornar, les comandes d'aquestes dues hores es processen quan torni. El client no se n'assabenta mai.

Anatomia d'una cua i el model de sondeig

Quatre conceptes i prou. El productor envia missatges; a MercadoFresco, la botiga a asg-mercadofresco-tienda, que només necessita sqs:SendMessage i l'URL de la cua. La cua és el magatzem durador: SQS replica cada missatge en diversos servidors de diverses AZ d'eu-west-1 abans de confirmar l'enviament, i no té mida màxima ni capacitat que calgui aprovisionar. El missatge té un cos (Body) de fins a 256 KiB de text, fins a 10 atributs de missatge tipats que viatgen fora del cos, i atributs del sistema (MessageId, SentTimestamp, ApproximateReceiveCount). El consumidor llegeix, treballa i esborra; n'hi pot haver un o mil sense que es trepitgin.

La distinció entre cos i atributs importa: els atributs s'inspeccionen sense deserialitzar el cos i —clau a 07-02— SNS filtra per atributs. Regla de MercadoFresco: als atributs hi va el que serveix per decidir què fer amb el missatge; al cos, la dada.

{
  "MessageAttributes": {
    "tipo_evento":    { "DataType": "String", "StringValue": "PedidoConfirmado" },
    "franja_entrega": { "DataType": "String", "StringValue": "24h" },
    "version":        { "DataType": "Number", "StringValue": "1" }
  },
  "MessageBody": "{\"pedido_id\":\"PED-2026-084417\",\"cliente_id\":\"CLI-30912\",\"importe_eur\":48.20,\"franja_entrega\":\"24h\",\"lineas\":[{\"sku\":\"FRUT-FRES-011\",\"unidades\":2}],\"confirmado_en\":\"2026-08-02T18:41:07Z\"}"
}

El cos és una cadena, no un objecte: SQS no sap què hi ha a dins. La serialització i el versionatge del contracte són cosa teva, i hi tornarem a 07-03.

Sondeig (pull) davant d'inserció (push). Aquesta és la característica que defineix SQS: mai no truca a ningú. No hi ha manera de dir-li «quan arribi un missatge, envia'l a aquesta URL». És el consumidor qui pregunta.

Sondeig (SQS) Inserció (SNS, webhooks)
Qui inicia El consumidor pregunta El servei lliura
El ritme el marca El consumidor El productor
Si el consumidor està caigut Els missatges esperen Es perden o es reintenten a cegues
Contrapressió natural Sí: es llegeix el que es pot No: arriba el que arriba
Necessita punt final públic No Sí (llevat de destinacions internes d'AWS)
Reintent Implícit: si no esborres, torna Configurat per l'emissor

La conseqüència arquitectònica és enorme: la cua actua d'esmorteïdor. Si entren 900 comandes/hora i els consumidors en processen 600, no es perd res: la cua creix i a les 22:00 es buida. Aquesta propietat —la contrapressió— és la raó que les cues continuïn sent la peça bàsica d'integració, i la reprendrem a 07-05.

L'aparent excepció és Lambda: quan connectes una cua a una funció sembla que SQS «empenyi». No és així; el servei Lambda manté sondejadors propis que criden ReceiveMessage per tu. El model continua sent de sondeig, només que el sondejador el posa AWS.

Cues estàndard i cues FIFO

Cua estàndard Cua FIFO
Nom Lliure Ha d'acabar en .fifo
Ordre Best-effort: gairebé sempre, no garantit Estricte dins de cada grup
Lliurament Almenys un cop (pot duplicar) Exactament un cop dins de la finestra de deduplicació
Rendiment Pràcticament il·limitat 300 msg/s (3.000 amb lots); alt rendiment: desenes de milers
Grups No existeixen MessageGroupId obligatori
Deduplicació No Per MessageDeduplicationId o hash del cos, finestra de 5 min
Paral·lelisme Total Un missatge en vol per grup
Preu 0,40 USD/milió 0,50 USD/milió

Tres idees que convé interioritzar. «Almenys un cop» significa que hi haurà duplicats: no és teòric, SQS emmagatzema cada missatge en diversos servidors i ocasionalment un no s'assabenta que ja va ser esborrat i el torna a lliurar. Si el teu consumidor cobra una targeta, un duplicat és cobrar dues vegades, i la solució no és FIFO sinó la idempotència del consumidor (07-05).

L'«exactament un cop» de FIFO té lletra petita: només actua dins d'una finestra de 5 minuts i només cobreix l'enviament duplicat del mateix missatge; si el consumidor processa i mor abans d'esborrar, el missatge torna. FIFO redueix molt la probabilitat de duplicat; no l'elimina.

Els grups són la unitat d'ordre i de paral·lelisme alhora. Per garantir l'ordre, SQS no lliura un segon missatge del grup fins que el primer s'ha esborrat. Un únic grup per a tota la cua dona ordre global i un sol consumidor efectiu; el sku com a grup dona ordre per producte i tant paral·lelisme com productes hi hagi. És la mateixa decisió que triar clau de partició a DynamoDB (06-02).

Quina cua fa servir cada tasca de MercadoFresco

Tasca Cua Tipus Per què
Preparar la caixa cola-mercadofresco-almacen Estàndard El magatzem reconcilia per pedido_id; l'ordre entre comandes és igual
Correu de confirmació cola-mercadofresco-correo Estàndard Duplicar un correu és molest, no catastròfic; es deduplica al consumidor
Notificar el repartidor cola-mercadofresco-reparto Estàndard Ídem
Esdeveniment d'analítica cola-mercadofresco-analitica Estàndard Redshift agrega; els duplicats es filtren a la càrrega
Moviments d'estoc cola-mercadofresco-stock.fifo FIFO, grup = sku «Reservar 2» i «alliberar 2» del mateix SKU han d'anar en ordre
Miniatures de fotos cola-mercadofresco-miniaturas Estàndard Regenerar una miniatura és idempotent per naturalesa

Només una de les sis necessita FIFO, i és justament la que toca comptadors. És l'habitual: la majoria de les càrregues no necessiten ordre global, necessiten un consumidor idempotent. Pagar el preu de FIFO quan no cal és un error de disseny car.

En aquesta lliçó el Luis comença pel mínim viable: cola-mercadofresco-pedidos recull tota la feina posterior a la confirmació, i cola-mercadofresco-correo separa l'enviament de correus, el consumidor més lent i el que més falla. A 07-02 veurem per què aquesta cua única acaba sent un problema.

Cicle de vida d'un missatge

sequenceDiagram
    participant P as Productor (botiga)
    participant Q as cola-mercadofresco-pedidos
    participant C as Consumidor
    P->>Q: SendMessage(Body, MessageAttributes)
    Q-->>P: MessageId (replicat i durador)
    Note over Q: Estat: visible
    C->>Q: ReceiveMessage(Max=10, WaitTimeSeconds=20)
    Q-->>C: Missatges + ReceiptHandle
    Note over Q: Estat: en vol (invisible)<br/>durant VisibilityTimeout
    alt Feina correcta
        C->>Q: DeleteMessage(ReceiptHandle)
        Note over Q: El missatge desapareix
    else El consumidor falla o cau
        Note over Q: Expira el temps de visibilitat
        Note over Q: Visible un altre cop<br/>ApproximateReceiveCount += 1
        Q-->>C: Es torna a lliurar
    end

Tres estats i només tres: visible, en vol i esborrat. Amb això s'explica tot el comportament de SQS, inclosos els que semblen fallades del servei. La part que sorprèn sempre: rebre un missatge no l'elimina. SQS el lliura i l'amaga de la resta de consumidors, però continua allà; si el teu procés mor a mitges, si el contenidor es reinicia, si l'ASG apaga la instància, el missatge reapareix i un altre consumidor l'agafa. Aquesta és la garantia de durabilitat.

El corol·lari: no esborrar equival a reprocessar. Si el consumidor llança una excepció i no arriba al DeleteMessage, el missatge tornarà. I si triga més que el temps de visibilitat, tornarà encara que acabi bé, amb la qual cosa la feina es farà dues vegades: la causa número u de duplicats en producció.

El temps d'espera de visibilitat, en detall

El VisibilityTimeout és el nombre de segons que un missatge roman invisible després de ser rebut. Per defecte 30 s; màxim 12 hores. La regla és una sola frase:

El temps de visibilitat ha de ser més gran que el temps que triga el consumidor més lent a processar el missatge i esborrar-lo.

Vegem què passa si no es compleix. El consumidor de correu crida un SMTP que al p99 triga 42 s, i el temps de visibilitat està als 30 per defecte:

t Consumidor A Consumidor B Estat del missatge
0 s Rep PED-084417 En vol
5 s Crida l'SMTP En vol
30 s Continua esperant Torna a ser visible
31 s Continua esperant Rep el mateix missatge En vol (per a B)
42 s Respon l'SMTP, DeleteMessage Crida l'SMTP Esborrat
73 s DeleteMessageReceiptHandleIsInvalid

El client ha rebut dos correus. I la fallada és silenciosa, difícil de reproduir en proves i només apareix sota càrrega. Hi ha tres maneres de configurar-ho:

1. A la cua, com a valor per defecte:

aws sqs set-queue-attributes \
  --queue-url https://sqs.eu-west-1.amazonaws.com/111122223333/cola-mercadofresco-correo \
  --attributes VisibilityTimeout=90 --profile mercadofresco-dev --region eu-west-1

2. A la recepció, passant VisibilityTimeout a ReceiveMessage, útil quan el mateix consumidor atén feines de cost dispar.

3. Ampliant-lo sobre la marxa (heartbeat). Si la feina pot durar molt i no saps quant, no posis 12 hores: posa un valor curt i allarga'l mentre treballes amb ChangeMessageVisibility. Si el procés mor, el missatge torna en segons en lloc d'en hores.

def batec(receipt_handle, parar):
    """Amplia la visibilitat a 60 s cada 30 s mentre la feina segueixi en curs."""
    while not parar.wait(30):
        try:
            sqs.change_message_visibility(QueueUrl=CUA, ReceiptHandle=receipt_handle,
                                          VisibilityTimeout=60)  # 60 s DES D'ARA
        except sqs.exceptions.ReceiptHandleIsInvalid:
            break                       # ja s'ha esborrat o ha expirat: res a ampliar

def processar_amb_batec(missatge):
    parar = threading.Event()
    fil = threading.Thread(target=batec, args=(missatge["ReceiptHandle"], parar), daemon=True)
    fil.start()
    try:
        preparar_caixa_al_magatzem(missatge["Body"])   # de 2 s a 8 minuts
        sqs.delete_message(QueueUrl=CUA, ReceiptHandle=missatge["ReceiptHandle"])
    finally:
        parar.set()                                    # atura el batec passi el que passi
        fil.join(timeout=2)

Dos detalls: VisibilityTimeout=60 significa «invisible 60 segons des d'aquest moment», no «afegeix 60 al que quedava»; i el total acumulat des de la primera recepció no pot superar les 12 hores. La mateixa ordre serveix per retornar un missatge immediatament amb VisibilityTimeout=0 quan detectes que no el pots processar ara, o ajornar-lo cinc minuts amb 300.

Sondeig curt i sondeig llarg

Amb sondeig curt (WaitTimeSeconds=0, valor per defecte en una cua acabada de crear) SQS consulta un subconjunt de servidors i respon immediatament, encara que sigui amb una llista buida. Pots rebre una resposta buida amb missatges a la cua, i el teu bucle genera peticions a màxima velocitat. Amb sondeig llarg (1 a 20 s) SQS consulta tots els servidors i manté la connexió oberta fins que hi ha un missatge o s'esgota l'espera.

Sondeig curt Sondeig llarg (20 s)
Respostes buides amb cua no buida Possible No
Peticions/hora d'un consumidor ociós Fins a ~360.000 180
Latència en arribar un missatge Fins al sondeig següent Pràcticament immediata
Cost mensual de 4 consumidors ociosos ~415 USD ~0,21 USD
Quan fer-lo servir Gairebé mai Sempre

Els 415 USD no són retòrica: quatre processos sondejant en bucle generen de l'ordre de mil milions de peticions al mes. És l'error de novell més car de SQS i apareix a Cost Explorer (11-03) com una línia desproporcionada. Configura'l a la cua i a cada crida:

aws sqs set-queue-attributes \
  --queue-url https://sqs.eu-west-1.amazonaws.com/111122223333/cola-mercadofresco-pedidos \
  --attributes ReceiveMessageWaitTimeSeconds=20 --profile mercadofresco-dev --region eu-west-1

Si fas servir sondeig llarg, apuja el temps d'espera de lectura del client HTTP per sobre dels 20 segons o l'SDK tallarà la connexió abans d'hora.

Retenció, retard, temporitzadors i càrregues grans

Atribut Rang Per defecte Ús a MercadoFresco
MessageRetentionPeriod 60 s – 14 dies 4 dies 14 dies a les DLQ, 4 a les normals
DelaySeconds de cua 0 – 15 min 0 10 s a cola-mercadofresco-correo
DelaySeconds per missatge 0 – 15 min 900 s a «la teva comanda surt del magatzem»
MaximumMessageSize 1 KiB – 256 KiB 256 KiB Per defecte

Retenció. Un missatge que ningú no esborra s'elimina en complir el període. És una xarxa de seguretat, no una funció: si els teus missatges caduquen, tens un consumidor aturat i una alarma que no va saltar. A les DLQ s'hi posa el màxim perquè allà el missatge espera que un humà se'l miri.

Cua de retard. Amb DelaySeconds a escala de cua, tots els missatges neixen invisibles. El Luis ho fa servir a la cua de correu amb 10 segons: dona marge que l'escriptura a Aurora es repliqui a aurora-mf-lector-1/-2 abans que el consumidor llegeixi la comanda. Sense aquest marge, un 0,3 % dels correus sortien amb «comanda no trobada».

Temporitzador per missatge. El mateix efecte però per missatge, amb DelaySeconds a SendMessage. No està disponible en cues FIFO, on només existeix el retard a escala de cua.

Càrregues grans. 256 KiB és molt per a una comanda i poc per a una factura PDF. La solució no és comprimir: és la biblioteca de client estesa (Extended Client Library), que desa la càrrega a S3 i envia per la cua només un punter {"s3_bucket": ..., "s3_key": ...}. A mà són tres línies: un put_object a mercadofresco-informes-analitica i un send_message amb la referència. Compte amb el cicle de vida: si el consumidor esborra el missatge i ningú no esborra l'objecte, pagues emmagatzematge indefinidament; una regla de cicle de vida d'S3 (02-03) amb caducitat a 7 dies ho resol.

Cues de missatges fallits i redrive

Un missatge que el consumidor no pot processar torna a la cua. Si la causa és transitòria —l'ERP caigut— és justament el que vols. Si és permanent —JSON mal format, sku inexistent, un None on hi havia d'haver un número— el missatge tornarà per sempre, consumint peticions, bloquejant l'ordre en cues FIFO i embrutant els registres. És un missatge enverinat (poison pill).

La cua de missatges fallits (DLQ) és una cua normal on SQS mou els missatges rebuts massa vegades. Es configura amb una política de redrive a la cua d'origen:

{
  "deadLetterTargetArn": "arn:aws:sqs:eu-west-1:111122223333:mercadofresco-miniaturas-fallidas",
  "maxReceiveCount": 3
}

maxReceiveCount compta recepcions, no fallades: amb 3, el missatge s'intenta tres vegades i a la quarta recepció es mou.

maxReceiveCount Efecte
1 Sense reintents: qualsevol fallada transitòria envia el missatge a la DLQ. Gairebé mai correcte
3 – 5 Recomanat: absorbeix caigudes curtes i aïlla ràpid els missatges enverinats
50+ L'enverinat es reintenta durant hores; els registres s'omplen de soroll

Aquí formalitzem mercadofresco-miniaturas-fallidas, que va aparèixer al mòdul 2 al costat de mercadofresco-generar-miniaturas i mai no es va configurar del tot. L'arquitectura queda així: en pujar una foto a mercadofresco-catalogo-fotos, S3 publica a cola-mercadofresco-miniaturas; la Lambda consumeix; si falla tres vegades, el missatge acaba a mercadofresco-miniaturas-fallidas.

aws sqs set-queue-attributes \
  --queue-url https://sqs.eu-west-1.amazonaws.com/111122223333/cola-mercadofresco-miniaturas \
  --attributes "{\"RedrivePolicy\":\"{\\\"deadLetterTargetArn\\\":\\\"$DLQ_ARN\\\",\\\"maxReceiveCount\\\":\\\"3\\\"}\"}" \
  --profile mercadofresco-dev --region eu-west-1

Aquest escapament triple és tan lleig com sembla: RedrivePolicy és un JSON dins d'una cadena dins d'un altre JSON. A 09-01 l'escriurem en CloudFormation i el problema desapareix.

Què mirar quan alguna cosa cau a la DLQ, en aquest ordre: (1) l'ApproximateReceiveCount —si és exactament maxReceiveCount + 1, va esgotar els reintents—; (2) el cos: JSON vàlid?, camps esperats?, versió del contracte coneguda?; (3) els registres de CloudWatch del consumidor filtrats pel MessageId. Aquí es paga haver posat el MessageId a cada línia de registre.

Redrive. Corregida la fallada, no cal reenviar a mà:

aws sqs start-message-move-task \
  --source-arn arn:aws:sqs:eu-west-1:111122223333:mercadofresco-miniaturas-fallidas \
  --max-number-of-messages-per-second 20 --profile mercadofresco-dev --region eu-west-1

Sense --destination-arn els missatges tornen a la seva cua d'origen. El límit de velocitat no és opcional a la pràctica: reprocessar 40.000 missatges de cop pot tombar el sistema que ja era fràgil. A 07-05 escriurem el runbook complet de la Marta.

Regla dura de MercadoFresco: tota cua té DLQ, i tota DLQ té alarma. Una DLQ sense alarma és una galleda d'escombraries on es perden vendes en silenci.

Crear les cues per CLI, política de cua i xifratge

Comencem per la DLQ, perquè la cua principal necessita el seu ARN.

export PERFIL="--profile mercadofresco-dev --region eu-west-1"
export ETIQUETES='Proyecto=mercadofresco,Entorno=produccion,Componente=integracion,Propietario=marta,CentroCoste=tecnologia'

aws sqs create-queue --queue-name mercadofresco-pedidos-fallidos \
  --attributes MessageRetentionPeriod=1209600 --tags "$ETIQUETES" $PERFIL
DLQ=$(aws sqs get-queue-attributes \
  --queue-url https://sqs.eu-west-1.amazonaws.com/111122223333/mercadofresco-pedidos-fallidos \
  --attribute-names QueueArn --query 'Attributes.QueueArn' --output text $PERFIL)

aws sqs create-queue --queue-name cola-mercadofresco-pedidos --tags "$ETIQUETES" $PERFIL \
  --attributes "{\"VisibilityTimeout\":\"120\",\"MessageRetentionPeriod\":\"345600\",
    \"ReceiveMessageWaitTimeSeconds\":\"20\",\"KmsMasterKeyId\":\"alias/mercadofresco-datos\",
    \"KmsDataKeyReusePeriodSeconds\":\"300\",
    \"RedrivePolicy\":\"{\\\"deadLetterTargetArn\\\":\\\"$DLQ\\\",\\\"maxReceiveCount\\\":\\\"4\\\"}\"}"

aws sqs create-queue --queue-name cola-mercadofresco-correo --tags "$ETIQUETES" $PERFIL \
  --attributes '{"VisibilityTimeout":"90","DelaySeconds":"10",
                 "ReceiveMessageWaitTimeSeconds":"20",
                 "KmsMasterKeyId":"alias/mercadofresco-datos"}'

Cada valor respon a un mesurament: VisibilityTimeout=120 a comandes perquè el consumidor de l'ERP triga 6,5 s al p95 i 38 s al pitjor cas observat; 90 a correu perquè l'SMTP arriba a 42 s al p99; maxReceiveCount=4 per a tres reintents reals abans d'aïllar; i KmsDataKeyReusePeriodSeconds=300, que fa que SQS reutilitzi la clau de dades 5 minuts i redueix dràsticament les crides a KMS sense comprometre la seguretat de manera apreciable. La cua FIFO d'estoc es crea diferent:

aws sqs create-queue --queue-name cola-mercadofresco-stock.fifo \
  --attributes '{"FifoQueue":"true","ContentBasedDeduplication":"false",
                 "DeduplicationScope":"messageGroup","FifoThroughputLimit":"perMessageGroupId",
                 "VisibilityTimeout":"60","ReceiveMessageWaitTimeSeconds":"20"}' \
  --tags "$ETIQUETES" $PERFIL

DeduplicationScope=messageGroup amb FifoThroughputLimit=perMessageGroupId activen el mode d'alt rendiment: el límit passa a aplicar-se per grup en lloc de per cua. Amb el sku com a grup i 3.400 SKU, el paral·lelisme és més que suficient.

Les dues capes d'autorització. La política d'identitat (IAM, 04-01) s'adjunta al rol que crida. La política de cua (basada en recurs) s'adjunta a la cua i és imprescindible quan qui envia no és un principal IAM del teu compte: un altre servei d'AWS (SNS, S3, EventBridge) o un altre compte.

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "PermitirAvisosDeS3Catalogo",
    "Effect": "Allow",
    "Principal": { "Service": "s3.amazonaws.com" },
    "Action": "sqs:SendMessage",
    "Resource": "arn:aws:sqs:eu-west-1:111122223333:cola-mercadofresco-miniaturas",
    "Condition": {
      "ArnLike":      { "aws:SourceArn": "arn:aws:s3:::mercadofresco-catalogo-fotos" },
      "StringEquals": { "aws:SourceAccount": "111122223333" }
    }
  }]
}

aws:SourceArn limita el permís a aquell bucket; sense ell, qualsevol bucket d'S3 del món podria escriure a la teva cua. aws:SourceAccount tanca el problema del diputat confós, en què un servei d'AWS actua legítimament en nom d'un tercer (04-01).

Xifratge. SQS ofereix SSE-SQS (claus d'AWS, gratuït, actiu per defecte en cues noves) i SSE-KMS amb la teva clau. MercadoFresco fa servir alias/mercadofresco-datos a les cues amb dades personals —les comandes porten adreça de lliurament— per coherència amb el mòdul 4 i perquè les crides a KMS queden a trail-mercadofresco. Compte amb el detall que trenca desplegaments: si la cua està xifrada amb KMS, el productor necessita kms:GenerateDataKey i el consumidor kms:Decrypt sobre la clau, no només sobre la cua; i si el productor és un servei d'AWS, la política de la clau l'hi ha de permetre.

Productor i consumidor en Python

import json, os
from datetime import datetime, timezone
import boto3
from botocore.config import Config

# Un sol client reutilitzat: crear-lo per peticio resol credencials i obre
# connexions noves cada cop, un error de rendiment classic.
sqs = boto3.client("sqs", config=Config(
    region_name="eu-west-1",
    retries={"max_attempts": 5, "mode": "standard"},  # reintents davant de 5xx i estrangulament
    connect_timeout=2, read_timeout=5,                # acoten el pitjor cas de l'enviament
))
CUA_COMANDES = os.environ["URL_COLA_PEDIDOS"]


def confirmar_comanda(cistella, client, targeta):
    """Confirma una comanda: cobra, persisteix i encua. Res mes."""
    cobrament = passarella.cobrar(targeta, cistella.import_eur)              # imprescindible
    comanda_id = aurora.inserir_comanda(cistella, client, cobrament.referencia)  # imprescindible
    cos = {
        "version": 1, "pedido_id": comanda_id, "cliente_id": client.id,
        "importe_eur": float(cistella.import_eur),
        "franja_entrega": cistella.franja,         # "24h" o "estandar"
        "referencia_cobro": cobrament.referencia,
        "lineas": [{"sku": l.sku, "unidades": l.unitats} for l in cistella.linies],
        "confirmado_en": datetime.now(timezone.utc).isoformat(),
    }
    r = sqs.send_message(
        QueueUrl=CUA_COMANDES,
        MessageBody=json.dumps(cos, ensure_ascii=False),
        MessageAttributes={
            "tipo_evento":    {"DataType": "String", "StringValue": "PedidoConfirmado"},
            "franja_entrega": {"DataType": "String", "StringValue": cistella.franja},
            "version":        {"DataType": "Number", "StringValue": "1"},
        },
    )
    log.info("comanda encuada", extra={"pedido_id": comanda_id, "message_id": r["MessageId"]})
    return comanda_id

Tres decisions que mereixen comentari. El send_message va després de l'INSERT: si s'encués primer i l'INSERT fallés, hi hauria un missatge anunciant una comanda inexistent. En aquest ordre la fallada possible és la contrària —comanda escrita i missatge no enviat—, menys greu però real, i té nom: el problema de la doble escriptura, que resoldrem amb el patró outbox a 07-05. Els reintents de l'SDK estan configurats amb retrocés exponencial davant de 5xx. I els temps d'espera són curts: sense ells botocore fa servir 60 segons, de sobres per esgotar el pool de connexions de la botiga durant un incident de SQS.

Per a volums alts —la càrrega nocturna encua 40.000 moviments d'estoc— fes servir send_message_batch, que accepta 10 missatges per petició (Entries amb Id i MessageBody) i divideix la factura per deu. El parany: retorna HTTP 200 encara que hi hagi entrades fallides, així que cal recórrer r.get("Failed", []) i registrar cada fallada. Si no ho fas, perds missatges sense assabentar-te'n.

El consumidor que corre en una instància o contenidor té una forma canònica que convé copiar tal com és:

_continuar = True

def _aturar(signum, frame):
    """Aturada ordenada: amb SIGTERM de l'ASG o d'ECS, acaba el lot en curs."""
    global _continuar
    _continuar = False

signal.signal(signal.SIGTERM, _aturar)


def bucle_de_consum():
    while _continuar:
        resposta = sqs.receive_message(
            QueueUrl=CUA,
            MaxNumberOfMessages=10,         # el maxim: menys peticions, menys cost
            WaitTimeSeconds=20,             # sondeig llarg: imprescindible
            MessageAttributeNames=["All"],  # sense aixo, MessageAttributes arriba buit
            AttributeNames=["ApproximateReceiveCount", "SentTimestamp"],
        )
        missatges = resposta.get("Messages", [])   # la clau NO existeix si no hi ha missatges!
        if not missatges:
            continue

        esborrables = []
        for m in missatges:
            intents = int(m["Attributes"]["ApproximateReceiveCount"])
            try:
                processar_comanda(json.loads(m["Body"]), intents)
                esborrables.append({"Id": m["MessageId"], "ReceiptHandle": m["ReceiptHandle"]})
            except ErrorPermanent as e:
                # Dades invalides: reintentar no arregla res. S'arxiva i s'esborra perque
                # no consumeixi quatre recepcions abans d'acabar a la DLQ.
                log.error("missatge invalid", extra={"message_id": m["MessageId"], "error": str(e)})
                arxivar_per_revisio(m)
                esborrables.append({"Id": m["MessageId"], "ReceiptHandle": m["ReceiptHandle"]})
            except Exception as e:
                # Transitori: NO s'esborra. Tornara despres del temps de visibilitat.
                log.warning("fallada transitoria", extra={"message_id": m["MessageId"],
                                                          "intents": intents, "error": str(e)})

        if esborrables:
            r = sqs.delete_message_batch(QueueUrl=CUA, Entries=esborrables)
            for fallada in r.get("Failed", []):
                log.error("no s'ha pogut esborrar", extra={"id": fallada["Id"], "code": fallada["Code"]})

El que separa un consumidor correcte d'un que dona problemes al cap de tres setmanes: fer servir resposta.get("Messages", []) (la resposta no inclou la clau quan està buida, i resposta["Messages"] llança KeyError al primer sondeig); demanar MessageAttributeNames=["All"] o els atributs arribaran buits; distingir error permanent de transitori; esborrar per lots; gestionar SIGTERM perquè l'ASG no mati el procés a mig missatge; i fer servir ApproximateReceiveCount com a senyal per registrar més detall o passar a un camí degradat al tercer intent.

Consumir des de Lambda: mapatge d'origen d'esdeveniments

Per al consumidor de correu, muntar instàncies és desproporcionat: feina curta, esporàdica i sense estat. La connexió entre cua i funció s'anomena mapatge d'origen d'esdeveniments.

aws lambda create-event-source-mapping \
  --function-name mercadofresco-enviar-correo-pedido \
  --event-source-arn arn:aws:sqs:eu-west-1:111122223333:cola-mercadofresco-correo \
  --batch-size 10 --maximum-batching-window-in-seconds 5 \
  --scaling-config MaximumConcurrency=20 \
  --function-response-types ReportBatchItemFailures $PERFIL
Paràmetre Què fa Valor a MercadoFresco
--batch-size Missatges per invocació (1–10.000) 10: l'SMTP no es beneficia de més
--maximum-batching-window-in-seconds Espera a omplir el lot (0–300) 5: menys invocacions en vall
MaximumConcurrency Topall d'instàncies concurrents (2–1.000) 20: protegeix el límit de l'SMTP
--function-response-types Activa les fallades parcials de lot ReportBatchItemFailures

El topall de concurrència és la peça defensiva clau. Lambda escala agressivament amb la profunditat de la cua: pot passar de 5 a 60 sondejadors en un minut. Si al darrere hi ha Aurora amb 200 connexions o un SMTP amb límit de 50 enviaments per segon, aquesta elasticitat es converteix en una denegació de servei que et fas a tu mateix. És la mateixa idea de contrapressió que generalitzarem a 07-05.

El problema del lot sencer. Per defecte, si la funció llança una excepció, Lambda considera fallit tot el lot i no esborra cap dels deu missatges: si nou anaven bé i un malament, els nou es reprocessen. ReportBatchItemFailures ho arregla retornant només els que han fallat.

import json

def handler(event, context):
    """Consumidor de cola-mercadofresco-correo amb fallades parcials de lot."""
    fallits = []
    for registre in event["Records"]:
        try:
            comanda = json.loads(registre["body"])
            franja = registre.get("messageAttributes", {}) \
                             .get("franja_entrega", {}).get("stringValue", "estandar")
            enviar_correu_confirmacio(comanda["cliente_id"], comanda["pedido_id"],
                                      comanda["importe_eur"], franja)
        except Exception as e:
            print(json.dumps({"nivell": "ERROR", "message_id": registre["messageId"],
                              "error": str(e)}))
            # Nomes aquest missatge torna a la cua; la resta del lot s'esborra.
            fallits.append({"itemIdentifier": registre["messageId"]})
    return {"batchItemFailures": fallits}

Dues condicions perquè funcioni: declarar ReportBatchItemFailures al mapatge i retornar l'estructura exacta {"batchItemFailures": [{"itemIdentifier": "..."}]}. Si el nom de la clau no és aquest, Lambda l'ignora en silenci.

A més: no cal cridar delete_message —Lambda esborra els missatges en acabar bé— i el temps de visibilitat de la cua ha de ser com a mínim 6 vegades el timeout de la funció, que és la recomanació d'AWS i evita que Lambda rebi el mateix missatge mentre encara el processa.

Mètriques, alarmes i autoescalat per profunditat de cua

SQS publica mètriques a CloudWatch cada minut i sense cost addicional.

Mètrica Què significa Senyal
ApproximateNumberOfMessagesVisible Missatges esperant consumidor Profunditat de la cua
ApproximateNumberOfMessagesNotVisible Missatges en vol Feina en curs
ApproximateAgeOfOldestMessage Segons del missatge més antic La mètrica de salut real
NumberOfMessagesSent / Deleted Flux Sent > Deleted sostingut = s'acumula
NumberOfEmptyReceives Sondeigs buits Alt = sondeig curt = factura
SentMessageSize Mida mitjana A prop de 256 KiB = passar a S3

Si només en pots vigilar una, vigila ApproximateAgeOfOldestMessage. La profunditat enganya: 5.000 missatges amb consumidors ràpids és normal un divendres a les 19:00, i 40 missatges aturats una hora és un incident. L'antiguitat respon a la pregunta que importa: quant triga avui una comanda a arribar al magatzem?

aws cloudwatch put-metric-alarm --alarm-name mercadofresco-pedidos-cola-retrasada \
  --namespace AWS/SQS --metric-name ApproximateAgeOfOldestMessage \
  --dimensions Name=QueueName,Value=cola-mercadofresco-pedidos \
  --statistic Maximum --period 60 --evaluation-periods 3 --threshold 300 \
  --comparison-operator GreaterThanThreshold --treat-missing-data notBreaching \
  --alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco $PERFIL

La segona alarma és la mateixa ordre canviant la mètrica a ApproximateNumberOfMessagesVisible, la dimensió a mercadofresco-pedidos-fallidos, --threshold 0 i --period 300: qualsevol missatge a la DLQ és un incident. --treat-missing-data notBreaching és important a totes dues, perquè SQS deixa de publicar mètriques d'una cua que fa hores que està buida i sense aquest ajust l'alarma passaria a INSUFFICIENT_DATA generant soroll nocturn. Totes dues s'afegeixen al tauler mercadofresco-produccion.

Autoescalat per profunditat. L'ASG de treballadors asg-mercadofresco-trabajadores no ha d'escalar per CPU: un consumidor que espera l'ERP té la CPU al 4 % i està saturat. La mètrica correcta és el retard per instància.

def publicar_retard_per_instancia():
    """S'executa cada minut (EventBridge Scheduler, que veurem a 07-03)."""
    a = sqs.get_queue_attributes(QueueUrl=CUA, AttributeNames=[
        "ApproximateNumberOfMessages", "ApproximateNumberOfMessagesNotVisible"])["Attributes"]
    pendents = int(a["ApproximateNumberOfMessages"]) + \
               int(a["ApproximateNumberOfMessagesNotVisible"])
    grup = asg.describe_auto_scaling_groups(
        AutoScalingGroupNames=["asg-mercadofresco-trabajadores"])["AutoScalingGroups"][0]
    en_servei = max(1, sum(1 for i in grup["Instances"]
                           if i["LifecycleState"] == "InService"))
    cw.put_metric_data(Namespace="MercadoFresco/Tienda", MetricData=[{
        "MetricName": "MensajesPendientesPorInstancia",
        "Dimensions": [{"Name": "Cola", "Value": "cola-mercadofresco-pedidos"}],
        "Value": pendents / en_servei, "Unit": "Count"}])

Sobre aquesta mètrica es defineix una política de seguiment d'objectiu amb valor 35. El número no és arbitrari: un treballador processa uns 7 missatges per minut contra l'ERP i la Marta vol buidar la cua en 5 minuts com a màxim, així que 7 × 5 = 35.

Cost real i neteja

SQS cobra exclusivament per petició a l'API, amb el primer milió mensual gratis de manera permanent. Un enviament, una recepció (porti 0 o 10 missatges) i un esborrat són una petició cadascun; les operacions per lots compten com una de sola, que és la raó econòmica de fer-les servir sempre.

Concepte Peticions/mes
Enviaments a la cua de comandes (240.000 comandes/mes) 240.000
Recepcions amb sondeig llarg i lots de 10 190.000
Esborrats per lots 24.000
Cua de correu (enviament + recepció + esborrat) 300.000
Miniatures i estoc 160.000
Total ~914.000 → dins del milió gratuït
KMS (GenerateDataKey, reutilització 300 s) ~8.600 → 0,03 USD

Cost total: pràcticament zero. Tot el desacoblament de MercadoFresco cap al nivell gratuït permanent, i l'única despesa apreciable són les crides a KMS. A canvi, 2.500 ms menys per petició alliberen fils a asg-mercadofresco-tienda molt abans i el grup escala a 3 en lloc de a 4 els divendres. Dues maneres de convertir aquest zero en una factura desagradable: sondeig curt en bucle (uns 100 USD al mes per procés ociós) i un KmsDataKeyReusePeriodSeconds baix amb molts consumidors.

for C in cola-mercadofresco-pedidos cola-mercadofresco-correo \
         cola-mercadofresco-stock.fifo mercadofresco-pedidos-fallidos; do
  aws sqs delete-queue --queue-url https://sqs.eu-west-1.amazonaws.com/111122223333/$C $PERFIL
done
aws lambda delete-event-source-mapping --uuid <UUID_DEL_MAPATGE> $PERFIL
aws cloudwatch delete-alarms --alarm-names mercadofresco-pedidos-cola-retrasada \
  mercadofresco-pedidos-dlq-con-mensajes $PERFIL

Esborrar una cua triga fins a 60 segons i no se'n pot crear una altra amb el mateix nom durant aquest minut, un detall molest en scripts que esborren i recreen.

Errors Habituals i Consells

Temps de visibilitat més curt que el processament. L'error número u. Símptoma: duplicats aleatoris que creixen amb la càrrega i ReceiptHandleIsInvalid als registres. Mesura el p99 del teu consumidor, multiplica'l per dos, i si no el pots acotar fes servir batec amb ChangeMessageVisibility.

Deixar el sondeig curt per defecte. Una cua creada per consola té ReceiveMessageWaitTimeSeconds=0. Posa'l a 20 a la cua i a cada crida.

Suposar que un missatge arriba un sol cop. Les cues estàndard dupliquen per disseny. Tot consumidor amb efectes externs —cobrar, enviar correu, cridar un soci— ha de ser idempotent (07-05).

Crear una cua FIFO «per si de cas». Limita el rendiment, complica el consumidor i indueix a creure que ja no calen reintents idempotents. I un sol MessageGroupId per a tota la cua FIFO converteix una cua distribuïda en un procés en sèrie: un missatge en vol com a màxim, tinguis els consumidors que tinguis.

Cua sense DLQ. Un missatge enverinat es reintenta fins a esgotar la retenció i —en FIFO— bloqueja tot el seu grup durant dies. I DLQ sense alarma és pitjor que no tenir-ne: dona falsa sensació de control mentre les comandes s'acumulen en silenci.

Llegir resposta["Messages"] sense .get() dona KeyError al primer sondeig buit; oblidar MessageAttributeNames=["All"] fa que els atributs arribin buits i el consumidor decideixi amb valors per defecte; i ignorar Failed a send_message_batch i delete_message_batch amaga fallades parcials sota un HTTP 200.

Xifrar amb KMS i oblidar els permisos de clau. El símptoma és un AccessDenied confús que esmenta KMS i no SQS.

Consell: posa el MessageId a totes les línies de registre. Quan investiguis un missatge de la DLQ tres dies després, serà l'única cosa que et permetrà reconstruir la història.

Consell: una cua per tipus de feina. Una cua compartida fa que el consumidor lent de l'ERP endarrereixi els correus. Cues separades permeten visibilitat, concurrència i prioritats diferents. I no facis servir SQS per a prioritats: no existeix la prioritat de missatge; si necessites urgent i normal, crea dues cues i dona més consumidors a la urgent.

Exercicis

Exercici 1: dimensionar la cua del magatzem

El consumidor de cola-mercadofresco-almacen crida l'ERP del soci. La Marta mesura: p50 1.180 ms, p95 6.500 ms, p99 31 s, pitjor cas en un mes 74 s. L'ERP accepta com a màxim 12 peticions concurrents. El divendres entren 900 comandes/hora i cap comanda no pot trigar més de 10 minuts a arribar al magatzem.

Determina: (a) el VisibilityTimeout i la seva justificació; (b) si convé Lambda o instàncies; (c) els paràmetres del mapatge d'origen d'esdeveniments si tries Lambda; (d) maxReceiveCount i retenció de la DLQ; (e) dues alarmes amb llindars calculats a partir de les dades, no inventats.

Exercici 2: el diagnòstic del correu duplicat

Tres divendres seguits arriben queixes de correus de confirmació duplicats —dos, de vegades tres— sempre entre les 18:00 i les 21:00. Les dades: ApproximateNumberOfMessagesVisible oscil·la entre 0 i 40; ApproximateAgeOfOldestMessage màxim 55 s; la Lambda té timeout 60 s, durada p50 900 ms i p99 47 s; la cua té VisibilityTimeout 60 s, batch-size 10 i sense ReportBatchItemFailures; la DLQ està buida; apareixen Task timed out after 60.00 seconds ocasionals.

Explica: (a) les dues causes independents de duplicat; (b) per què només passa al pic; (c) per què la DLQ buida no és tranquil·litzadora; (d) les correccions amb valors concrets; (e) quina elimina el problema d'arrel i quina només en redueix la freqüència.

Exercici 3: FIFO per a l'estoc

Dissenya cola-mercadofresco-stock.fifo. Els missatges són {"sku": "...", "delta": -2, "pedido_id": "...", "motivo": "reserva|liberacion|reposicion"}. Hi ha 3.400 SKU i al pic es generen 2.800 moviments per hora, concentrats en els 200 SKU més venuts.

Respon: (a) què faries servir com a MessageGroupId i quines tres alternatives descartes; (b) què faries servir com a MessageDeduplicationId i si activaries ContentBasedDeduplication; (c) què passa si un missatge del SKU FRUT-FRES-011 és enverinat i com ho mitigues; (d) si el mode d'alt rendiment és adequat; (e) per què aquesta cua no pot fer servir DelaySeconds per missatge i què faries si et calgués.

Solucions

Solució 1

(a) El pitjor cas és 74 s, així que 180 segons és defensable: 2,4 vegades el pitjor cas registrat. No convé pujar-lo a 900, perquè si el treballador mor el missatge quedaria bloquejat 15 minuts i l'objectiu és de 10. Alternativa superior: 120 s amb batec, que dona recuperació ràpida davant d'una caiguda del treballador sense límit superior real de durada.

(b) Lambda, amb reserves. A favor: feina esporàdica, sense estat i de durada variable; amb gairebé gens de trànsit de matinada, pagar instàncies enceses és absurd. En contra: el p99 de 31 s obliga a un timeout alt i una funció que espera es paga sencera. El factor decisiu és el límit de 12 concurrents de l'ERP: MaximumConcurrency l'imposa de manera declarativa, mentre que amb instàncies caldria un semàfor distribuït.

(c) --batch-size 1 (amb lots de 10 i 74 s per missatge, una invocació podria necessitar 740 s); --maximum-batching-window-in-seconds 0; MaximumConcurrency=12; timeout de la funció 120 s; i VisibilityTimeout de la cua 720 s, aplicant la regla de 6 × timeout, que aquí guanya a l'estimació de (a). Comprovació de capacitat: 12 ÷ 1,18 s = 10 comandes/s = 36.000/hora, i fins i tot al p95 (6,5 s) són 6.600/hora. Marge de sobres sobre 900.

(d) maxReceiveCount=4: amb 720 s de visibilitat, tres reintents cobreixen uns 36 minuts d'indisponibilitat abans d'aïllar. Retenció de la DLQ 14 dies, el màxim, perquè el que hi cau són vendes cobrades que la Marta ha de poder recuperar encara que l'incident sigui un divendres de pont.

(e) ApproximateAgeOfOldestMessage > 480 durant 2 períodes de 60 s: el requisit són 600 s, així que s'avisa als 8 minuts per deixar marge de reacció. I ApproximateNumberOfMessagesVisible > 0 a la DLQ. Una tercera recomanable: Errors de la funció > 10 en 5 minuts, que detecta l'ERP caigut abans.

Solució 2

(a) Causa 1: el temps de visibilitat és igual al timeout de la funció. Tots dos són 60 s: quan una invocació triga 47 s o esgota el timeout, el missatge es fa visible abans o just quan la funció acaba, i Lambda el lliura un altre cop. Falta el factor 6 recomanat. Causa 2: no està activat ReportBatchItemFailures. Amb batch-size 10, si el missatge número 7 falla o el lot esgota el timeout, cap dels deu no s'esborra i els altres nou es reprocessen; això explica els casos de tres duplicats.

(b) Totes dues causes depenen de la latència de l'SMTP, que es degrada quan s'envien molts correus alhora. En vall la funció triga 900 ms i no s'acosta a cap límit; a partir de les 18:00 la concurrència creix amb la profunditat de la cua, el proveïdor estrangula, la durada es dispara al p99 i les dues causes s'activen alhora. És una fallada que no es reprodueix en proves perquè només apareix amb concurrència real.

(c) Que la DLQ estigui buida significa que cap missatge no va esgotar les seves recepcions, no que tot vagi bé. Aquí la fallada és de doble lliurament amb èxit: el missatge es processa correctament dues vegades i s'esborra. Un duplicat reeixit mai no arriba a la DLQ. La DLQ detecta feina que no es va fer, mai feina que es va fer de més.

(d) VisibilityTimeout a 360 s (6 × 60); activar ReportBatchItemFailures i retornar batchItemFailures; MaximumConcurrency=20 per no estrangular l'SMTP; batch-size a 5 per acotar la durada per invocació; idempotència al consumidor escrivint PEDIDO#<id>#correo-confirmacion a DynamoDB amb escriptura condicional i TTL de 24 h; i alarmes sobre Duration p99 i Throttles.

(e) Les quatre primeres redueixen la freqüència: fan els duplicats molt més rars però no impossibles, perquè les cues estàndard dupliquen per disseny. L'única que elimina el problema d'arrel és la idempotència: rebre el missatge dues vegades passa a ser inofensiu. Aquesta és la tesi de 07-05, i el motiu que la configuració correcta sigui necessària però mai suficient.

Solució 3

(a) MessageGroupId = sku. L'ordre només importa entre moviments del mateix producte, i amb 3.400 SKU hi ha paral·lelisme de sobres. Descartades: un grup fix, que imposa ordre global innecessari i un sol missatge en vol; pedido_id, que garanteix l'ordre dins d'una comanda però no entre comandes diferents del mateix SKU, que és justament on hi ha la condició de cursa; i categoria, que deixa una dotzena de grups i crea grups calents a les categories més venudes —el mateix error que una clau de partició calenta a DynamoDB (06-02)—.

(b) Un identificador determinista i únic per moviment lògic: f"{pedido_id}:{sku}:{motivo}". Si el productor reintenta després d'un temps d'espera esgotat, el mateix moviment produeix el mateix identificador i SQS el descarta dins de la finestra de 5 minuts. ContentBasedDeduplication es deixa desactivat: el cos inclou una marca de temps, amb la qual cosa el hash canviaria entre reintents; i pitjor encara, dos moviments legítimament idèntics —dues reposicions de 10 unitats del mateix SKU a la mateixa finestra— es deduplicarien per error, perdent estoc real.

(c) És l'escenari més greu de FIFO: com que SQS no lliura el missatge següent del grup fins que l'actual s'esborra, tot el grup FRUT-FRES-011 queda bloquejat, i amb retenció de 4 dies aquell SKU passaria dies sense actualitzar estoc mentre els altres funcionen —una fallada parcial silenciosa—. Mitigació: DLQ amb maxReceiveCount=3; alarma sobre la DLQ; validació d'esquema al productor; distingir permanent de transitori al consumidor; i vigilar ApproximateAgeOfOldestMessage, que en FIFO delata un grup bloquejat encara que la profunditat total sigui baixa.

(d) Sí, i és gratuït. DeduplicationScope=messageGroup amb FifoThroughputLimit=perMessageGroupId aixeca el límit de 300 msg/s per cua. Encara que 2.800 moviments/hora són només 0,8 msg/s, activar-ho protegeix davant de campanyes o reposicions massives; l'única conseqüència és que la deduplicació passa a ser per grup, irrellevant perquè l'identificador ja inclou el sku.

(e) Les cues FIFO no admeten temporitzador per missatge, només el retard a escala de cua. Per ajornar un moviment concret —alliberar l'estoc d'una comanda no pagada als 15 minuts— hi ha tres opcions: DelaySeconds de cua, descartada perquè retardaria també les reserves urgents; retornar el missatge amb ChangeMessageVisibility, descartada perquè en FIFO bloqueja el grup sencer; i la correcta, treure l'ajornament de la cua i fer servir un estat Wait de Step Functions (07-04) o una regla programada d'EventBridge (07-03), de manera que el missatge només entri a la cua quan calgui aplicar-lo.

Conclusió

Confirmar una comanda a MercadoFresco ha passat de 2.893 ms de mediana a uns 400 ms al percentil 95, i de vuit punts de fallada encadenats a dos: cobrar la targeta i escriure a Aurora. La resta viu ara a cola-mercadofresco-pedidos i cola-mercadofresco-correo, amb cola-mercadofresco-stock.fifo per a l'única cosa que necessitava ordre real i mercadofresco-pedidos-fallidos recollint el que no es va poder processar. L'ERP del magatzem pot caure dues hores sense que es perdi ni una sola venda.

Pel camí has vist els conceptes que es repetiran tot el mòdul: el model de sondeig, que converteix la cua en esmorteïdor i dona contrapressió de franc; el temps d'espera de visibilitat, font número u de duplicats quan es queda curt; el fet que no esborrar és reprocessar, que és alhora la garantia de durabilitat i la raó que els consumidors hagin de ser idempotents; el sondeig llarg, que separa una factura de zero d'una de centenars d'euros; i les cues de missatges fallits amb el seu maxReceiveCount, la seva alarma obligatòria i el seu redrive.

Però ha quedat una costura a la vista. cola-mercadofresco-pedidos és un canal punt a punt: cada missatge el processa un consumidor i desapareix. Amb magatzem, correu, repartiment i analítica interessats en el mateix fet —«s'ha confirmat una comanda»—, o la botiga envia quatre missatges a quatre cues diferents —i aleshores torna a saber qui són els seus consumidors, just el que volíem evitar— o un consumidor únic reparteix la feina i es converteix en el nou punt de fallada. Quan el mes que ve màrqueting demani assabentar-se també de les comandes per al seu programa de fidelització, caldrà tornar a tocar el codi de la botiga. El desacoblament està a mitges.

A 07-02, «Amazon SNS», resoldrem aquesta meitat que falta: un tema al qual la botiga publica un cop i diverses cues subscrites que reben cadascuna la seva còpia. Veurem el patró fan-out SNS→SQS, per què convé posar una cua entre el tema i cada consumidor en lloc de subscriure funcions Lambda directament, com filtrar per atributs perquè la cua del repartiment en 24 hores només rebi el que li pertoca, i per què el tema alertas-mercadofresco que fem servir des del mòdul 5 és exactament el mateix mecanisme aplicat a les alarmes.

Curs d'AWS

Mòdul 1: Introducció a AWS

Mòdul 2: Serveis principals d'AWS

Mòdul 3: Xarxes i lliurament de contingut

Mòdul 4: Seguretat i identitat

Mòdul 5: Monitoratge i gestió

Mòdul 6: Bases de dades

Mòdul 7: Integració d'aplicacions

Mòdul 8: Eines per a desenvolupadors

Mòdul 9: Infraestructura com a codi i govern de comptes

Mòdul 10: Contenidors a AWS

Mòdul 11: Millors pràctiques i gestió de costos

© Copyright 2026. Tots els drets reservats