Automation
Azure Functions : diagnostiquer une poison queue avant de rejouer les messages
Un runbook de production pour qualifier les messages poison d’un Queue trigger, séparer panne déterministe et transitoire, prouver l’idempotence, puis rejouer ou rollbacker sans dupliquer les effets métier.
Une Azure Function alimentée par Storage Queue continue de traiter la majorité du trafic, mais sa file poison grossit après un déploiement. Remettre immédiatement les messages dans la file principale semble rétablir le flux. Cela peut aussi répéter une écriture déjà acceptée, masquer un défaut de schéma ou relancer en boucle un message que la nouvelle version ne saura jamais lire.
Le cas d’usage est un Queue trigger qui pilote une opération métier : génération de document, synchronisation de commande ou provisioning. Le runbook doit attribuer les échecs à un message, une version et une étape, puis décider entre correction, quarantaine, rejeu borné ou rollback du worker.
Figer le contrat et la fenêtre d’incident
Commencez par une seule Function et une seule queue. Capturez le bundle réellement déployé, la configuration du trigger, la fenêtre d’apparition et l’effet de bord attendu.
incident:
start: <timestamp>
first_poison_message: <timestamp>
function_app: func-orders-prod
function: ProcessOrder
queue_contract:
source: orders
poison: orders-poison
schema_version: <version>
business_key: orderId
expected_side_effect: create-or-update-order
active_bundle:
application_version: <immutable-version>
host_configuration: <commit-or-artifact-hash>
extension_bundle: <version-range>
app_settings_snapshot: <redacted-reference>
evidence:
- poison queue count and safe sample
- invocation, exception and dependency logs
- deployment and configuration timeline
- downstream state for sampled business keys
- previous deployable bundle Ne copiez pas les secrets des application settings dans le dossier d’incident. Conservez les noms, versions et empreintes utiles, puis référencez le coffre ou le mécanisme de configuration.
Échantillonner sans consommer la preuve
Un peek lit un échantillon sans rendre les messages invisibles ni modifier leur compteur de remise. Commencez ainsi ; n’utilisez pas une commande de récupération destructive pendant la qualification.
STORAGE_ACCOUNT="stprodorders"
POISON_QUEUE="orders-poison"
az storage message peek --account-name "$STORAGE_ACCOUNT" --queue-name "$POISON_QUEUE" --num-messages 16 --auth-mode login --output json
az monitor activity-log list --resource-group "rg-prod-orders" --offset 24h --query "[?contains(resourceId, 'func-orders-prod')].{time:eventTimestamp,operation:operationName.value,caller:caller,status:status.value}" --output table Pour chaque échantillon, relevez message ID, insertion time, business key, schema version, taille et empreinte du payload. Si le contenu est sensible, travaillez sur une copie expurgée. Le but est de regrouper les messages par signature d’échec, pas de constituer un export de données métier.
Distinguer cinq classes de panne
Une poison queue mélange souvent plusieurs causes. Classez avant de corriger :
- contrat déterministe : JSON invalide, champ obligatoire absent, version de schéma inconnue ;
- dépendance transitoire : timeout, throttling ou indisponibilité qui a dépassé la fenêtre de retry ;
- identité ou réseau : accès Storage, Key Vault, API ou base refusé pour la vraie identité d’exécution ;
- ressources : timeout Function, mémoire, scale ou durée de traitement incompatible avec la visibilité du message ;
- effet partiel : la dépendance a accepté l’écriture, puis l’invocation a échoué avant d’enregistrer son succès.
Le dernier cas est le plus dangereux. Une exception côté Function ne prouve pas que l’opération aval a échoué. Interrogez l’état métier ou le journal d’idempotence avant tout rejeu.
Corréler message, invocation et dépendance
La requête suivante suppose que l’application journalise messageId, businessKey, schemaVersion et appVersion dans les dimensions personnalisées. Adaptez les noms de tables à votre workspace et bornez toujours la fenêtre.
let StartTime = datetime(2026-08-05T08:00:00Z);
let EndTime = datetime(2026-08-05T10:00:00Z);
AppTraces
| where TimeGenerated between (StartTime .. EndTime)
| where AppRoleName == "func-orders-prod"
| extend MessageId = tostring(Properties.messageId),
BusinessKey = tostring(Properties.businessKey),
SchemaVersion = tostring(Properties.schemaVersion),
AppVersion = tostring(Properties.appVersion),
FailureClass = tostring(Properties.failureClass)
| where isnotempty(MessageId)
| summarize Attempts=count(),
FirstSeen=min(TimeGenerated),
LastSeen=max(TimeGenerated),
Messages=make_set(MessageId, 20)
by AppVersion, SchemaVersion, FailureClass, BusinessKey
| order by Attempts desc Corrélez ensuite l’invocation avec AppExceptions et AppDependencies. Une même exception sur une seule schema version indique un défaut déterministe. Des 429 ou timeouts concentrés sur une dépendance pointent vers un incident transitoire. Une dépendance en succès suivie d’une exception locale impose une vérification d’idempotence.
Vérifier retries et concurrence sans déplacer le problème
Le nombre maximal de tentatives, la visibilité, le batching et la concurrence forment un seul contrat d’exploitation. Une valeur plus élevée de retries ne corrige pas un payload illisible. Une concurrence accrue peut accélérer la saturation aval. Une visibilité trop courte peut rendre un message disponible alors que son traitement continue.
{
"extensions": {
"queues": {
"maxDequeueCount": "<derived-attempt-budget>",
"visibilityTimeout": "<greater-than-tested-processing-window>",
"batchSize": "<bounded-for-downstream-capacity>",
"newBatchThreshold": "<tested-concurrency-threshold>"
}
}
} Cet extrait est un modèle de décision, pas une configuration à copier. Vérifiez les options supportées par la version de l’extension réellement déployée. Dérivez le budget de tentatives de la durée acceptable, de la capacité aval et de la nature retentable des erreurs.
Corriger le défaut avant de préparer le rejeu
Pour un défaut de contrat, rendez le consumer compatible avec les versions attendues ou archivez explicitement les messages obsolètes. Pour une panne transitoire, prouvez la récupération de la dépendance. Pour un problème d’identité, testez avec l’identité de la Function, pas avec le compte d’un opérateur. Pour un effet partiel, ajoutez ou utilisez une clé d’idempotence stable.
{
"replay": {
"incidentId": "func-poison-20260805-01",
"originalMessageId": "<message-id>",
"businessKey": "<stable-business-key>",
"sourceSchemaVersion": "<version>",
"targetConsumerVersion": "<version>",
"replayAttempt": 1
},
"payload": "<validated-original-payload>"
} Ne modifiez pas silencieusement le payload historique. Si une transformation est nécessaire, versionnez-la, conservez l’empreinte de l’original et produisez un rapport dry-run montrant inclusion, exclusion et raison.
Rejouer par canary et avec condition d’arrêt
Le rejeu doit passer par un outil borné ou une queue de canary, jamais par une boucle qui vide toute la poison queue. Commencez par des messages sans effet irréversible et un seul exemplaire par signature.
replay:
incident_id: func-poison-20260805-01
include:
failure_class: transient_dependency
schema_versions: [v3]
inserted_at: [<start>, <end>]
exclude:
unknown_schema: true
business_keys_already_completed: true
irreversible_side_effect_unverified: true
canary:
messages: 5
pause_after_batch: 10m
acceptance:
- every replay has a new invocation correlation
- business state changes at most once
- no replayed message returns to poison
- latency and dependency errors stay inside baseline
stop:
- duplicate side effect detected
- unknown failure signature appears
- poison queue resumes growing
- correlation with downstream state is lost Une diminution du compteur de poison queue n’est pas un critère de succès. Réconciliez messages sélectionnés, invocations terminées et états métier réellement obtenus.
Décider, valider et rollbacker
Conservez la correction si le canary traite chaque signature prévue une seule fois, si les nouveaux messages ne rejoignent plus la poison queue et si les dépendances restent stables sous le débit normal. Élargissez ensuite par petits batchs avec les mêmes exclusions.
Rollbackez le worker si les poison messages apparaissent avec la nouvelle version et que le bundle précédent traite encore le contrat courant. Restaurez ensemble code, host.json et paramètres compatibles ; un rollback partiel peut conserver la mauvaise concurrence ou le mauvais budget de retry. Ne rejouez pas avant que le rollback ait traité un message synthétique et un échantillon expurgé.
Maintenez la quarantaine lorsque le schéma est inconnu, l’effet métier est ambigu ou l’idempotence manque. Le rollback d’une Function n’annule pas une écriture déjà acceptée par une dépendance.
Conclusion
Une poison queue n’est pas un simple backlog. Elle concentre les contrats incompatibles, les dépendances instables et les traitements partiels que le chemin nominal n’a pas su résoudre.
La décision de production doit rester vérifiable : qualifier chaque signature, contrôler l’état aval, corriger la cause, rejouer un canary borné puis conserver, étendre ou rollbacker. La file redevient alors un mécanisme de récupération, pas une deuxième voie d’écriture incontrôlée.