Aller au contenu principal

Corrections Production - Phase 1 Complétée

Date : 2026-01-24
Statut : ✅ Phase 1 Complétée

✅ Corrections Appliquées

1. ✅ Singleton OCR Thread-Safe

Problème : Singleton global non thread-safe
Solution : Double-checked locking pattern avec threading.Lock()

# Avant (non thread-safe)
_ocr_service_instance: Optional[OcrService] = None

# Après (thread-safe)
_ocr_service_instance: Optional[OcrService] = None
_ocr_service_lock = threading.Lock()

def get_ocr_service(...):
if _ocr_service_instance is None:
with _ocr_service_lock:
if _ocr_service_instance is None:
_ocr_service_instance = OcrService(...)
return _ocr_service_instance

2. ✅ Validation Taille Image

Problème : Pas de limite de taille d'image
Solution : Validation avec limite configurable (10MB par défaut)

# Configuration
OCR_MAX_IMAGE_SIZE_MB: int = 10 # Configurable via env var

# Validation
def _validate_image_size(self, image_data: bytes) -> None:
if len(image_data) > self.max_image_size_bytes:
raise OcrImageTooLargeError(...)

Erreur HTTP : 413 Payload Too Large


3. ✅ OCR Vraiment Async

Problème : readtext() est blocking, pas async
Solution : Utilisation de run_in_executor pour exécuter dans un thread pool

# Avant (blocking)
results = self.reader.readtext(image, detail=detail)

# Après (vraiment async)
loop = asyncio.get_event_loop()
results = await loop.run_in_executor(
None, self.reader.readtext, image, detail
)

4. ✅ Timeout Adaptatif

Problème : Timeout fixe (60s) inadapté
Solution : Timeout calculé selon la taille de l'image

// Calcul adaptatif
const imageSizeMB = imageBuffer.length / (1024 * 1024);
const baseTimeout = 30000; // 30s base
const timeoutPerMB = 5000; // +5s par MB
const maxTimeout = 120000; // Max 120s
const adaptiveTimeout = Math.min(
baseTimeout + imageSizeMB * timeoutPerMB,
maxTimeout
);

Formule : timeout = min(30s + (taille_MB × 5s), 120s)


5. ✅ Gestion d'Erreurs Granulaire

Problème : Erreurs génériques
Solution : Exceptions typées avec codes HTTP appropriés

# Exceptions typées
class OcrError(Exception): pass
class OcrImageTooLargeError(OcrError): pass # 413
class OcrDecodeError(OcrError): pass # 400
class OcrEngineError(OcrError): pass # 500

# Gestion dans le router
except OcrImageTooLargeError as e:
raise HTTPException(status_code=413, detail=str(e))
except OcrDecodeError as e:
raise HTTPException(status_code=400, detail=str(e))
except OcrEngineError as e:
raise HTTPException(status_code=500, detail=str(e))

6. ✅ Configuration OCR dans Settings

Problème : Configuration hardcodée
Solution : Configuration centralisée avec Pydantic Settings

# app/config.py
class Settings(BaseSettings):
OCR_ENGINE: str = "easyocr"
OCR_LANGUAGES: List[str] = ["en", "fr"]
OCR_MAX_IMAGE_SIZE_MB: int = 10
OCR_TIMEOUT_BASE_MS: int = 30000
OCR_TIMEOUT_PER_MB_MS: int = 5000

Variables d'environnement :

  • OCR_ENGINE=easyocr|paddleocr
  • OCR_LANGUAGES=en,fr
  • OCR_MAX_IMAGE_SIZE_MB=10
  • OCR_TIMEOUT_BASE_MS=30000
  • OCR_TIMEOUT_PER_MB_MS=5000

📊 Résultats

Avant

  • ❌ Singleton non thread-safe
  • ❌ Pas de validation taille
  • ❌ OCR blocking
  • ❌ Timeout fixe
  • ❌ Erreurs génériques
  • ❌ Configuration hardcodée

Après

  • ✅ Singleton thread-safe (double-checked locking)
  • ✅ Validation taille (10MB max, configurable)
  • ✅ OCR vraiment async (run_in_executor)
  • ✅ Timeout adaptatif (30s + 5s/MB, max 120s)
  • ✅ Erreurs typées avec codes HTTP appropriés
  • ✅ Configuration centralisée (Pydantic Settings)

🚀 Prochaines Étapes (Phase 2)

  1. ⏳ Optimiser Dockerfile (multi-stage build)
  2. ⏳ Ajouter rate limiting
  3. ⏳ Monitoring/Métriques (Prometheus)
  4. ⏳ Circuit breaker
  5. ⏳ Cache Redis

📝 Notes

  • Thread-safety : Le singleton utilise maintenant un lock pour éviter les race conditions
  • Performance : L'OCR est maintenant vraiment async, ne bloque plus le thread principal
  • Robustesse : Les erreurs sont maintenant typées et gérées de manière granulaire
  • Configurabilité : Tous les paramètres sont maintenant configurables via variables d'environnement

Statut Production : ✅ Phase 1 complétée - Prêt pour MVP