Fastly KV Store Race Condition sur Écritures Concurrentes
Problème
Quand plusieurs requêtes concurrentes effectuent des opérations lecture-modification-écriture sur la même clé Fastly KV Store, les écritures peuvent échouer avec des erreurs 500 intermittentes. Cela se produit couramment lors de chargements en lot où chaque requête met à jour une liste ou un compteur partagé.
Contexte / Conditions de Déclenchement
- Le message d'erreur contient « Failed to store list: » ou une erreur KV write similaire
- Taux d'échec de ~10-20% lors d'opérations concurrentes
- Plusieurs requêtes du même utilisateur/session mettant à jour l'état partagé
- Modèle :
read key → modify data → write keysans aucun verrouillage
Exemple d'erreur :
{"error":"Failed to store list:"}
Solution
Ajoutez une boucle de retry qui relit les données avant chaque tentative d'écriture :
/// Ajouter un élément à la liste avec retry pour les conflits d'écriture concurrente
pub fn add_to_list(key: &str, item: &str) -> Result<()> {
// Retry jusqu'à 5 fois pour les conflits d'écriture concurrente
for attempt in 0..5 {
// Relire l'état courant à chaque tentative
let mut items = get_list(key)?;
if items.contains(&item.to_string()) {
return Ok(()); // Existe déjà, terminé
}
items.push(item.to_string());
match put_list(key, &items) {
Ok(()) => return Ok(()),
Err(e) if attempt < 4 => {
eprintln!("[KV] Retry {} for list update: {}", attempt + 1, e);
// La relecture capte les écritures concurrentes
continue;
}
Err(e) => return Err(e),
}
}
Err(Error::new("Max retries exceeded for list update"))
}
Points clés :
- Relire à chaque retry - La lecture fraîche capte les changements des écritures concurrentes
- Vérifier les doublons - Éviter d'ajouter deux fois le même élément
- Logger les retries - Aide à déboguer si les problèmes persistent
- Limiter les retries - 5 tentatives suffisent généralement
Vérification
- Les opérations en lot qui échouaient précédemment ~16% doivent maintenant réussir ~100%
- Les messages de log de retry (
[KV] Retry N for...) indiquent que le mécanisme fonctionne - En cas d'échec après 5 retries, il peut y avoir un problème plus profond
Exemple
Avant (race condition) :
pub fn add_to_user_list(pubkey: &str, hash: &str) -> Result<()> {
let mut hashes = get_user_blobs(pubkey)?; // Read
if !hashes.contains(&hash) {
hashes.push(hash.to_string()); // Modify
put_user_list(pubkey, &hashes)?; // Write - CONFLICT!
}
Ok(())
}
Après (avec retry) :
pub fn add_to_user_list(pubkey: &str, hash: &str) -> Result<()> {
for attempt in 0..5 {
let mut hashes = get_user_blobs(pubkey)?;
if hashes.contains(&hash) {
return Ok(());
}
hashes.push(hash.to_string());
match put_user_list(pubkey, &hashes) {
Ok(()) => return Ok(()),
Err(e) if attempt < 4 => continue,
Err(e) => return Err(e),
}
}
Err(Error::new("Max retries exceeded"))
}
Notes
- Fastly Compute n'a pas de
sleep(), donc les retries se font immédiatement - La relecture fournit le « délai » en capturant les changements concurrents
- Envisagez des opérations atomiques si disponibles (Fastly KV ne supporte pas CAS)
- Pour les scénarios de forte contention, envisagez le sharding de l'espace de clés
- Ce modèle s'applique aussi aux opérations de suppression (read-filter-write)