# Graceful Shutdown Implementation - ASE **Data**: 2025-10-11 **Versione**: 0.9.0 ## 🎯 Obiettivo Implementare un meccanismo di graceful shutdown che permette all'applicazione di: 1. Ricevere segnali di terminazione (SIGTERM da systemd/docker, SIGINT da Ctrl+C) 2. Terminare ordinatamente tutti i worker in esecuzione 3. Completare le operazioni in corso (con timeout) 4. Chiudere correttamente le connessioni al database 5. Evitare perdita di dati o corruzione dello stato --- ## πŸ”§ Implementazione ### 1. Signal Handlers (`orchestrator_utils.py`) #### Nuovo Event Globale ```python shutdown_event = asyncio.Event() ``` Questo event viene usato per segnalare a tutti i worker che Γ¨ richiesto uno shutdown. #### Funzione setup_signal_handlers() ```python def setup_signal_handlers(logger: logging.Logger): """Setup signal handlers for graceful shutdown. Handles both SIGTERM (from systemd/docker) and SIGINT (Ctrl+C). """ def signal_handler(signum, frame): sig_name = signal.Signals(signum).name logger.info(f"Ricevuto segnale {sig_name} ({signum}). Avvio shutdown graceful...") shutdown_event.set() signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGINT, signal_handler) ``` **Segnali gestiti**: - `SIGTERM (15)`: Segnale standard di terminazione (systemd, docker stop, etc.) - `SIGINT (2)`: Ctrl+C dalla tastiera --- ### 2. Orchestrator Main Loop (`run_orchestrator`) #### Modifiche Principali **Prima**: ```python tasks = [asyncio.create_task(worker_coro(i, cfg, pool)) for i in range(cfg.max_threads)] await asyncio.gather(*tasks, return_exceptions=debug_mode) ``` **Dopo**: ```python tasks = [asyncio.create_task(worker_coro(i, cfg, pool)) for i in range(cfg.max_threads)] # Wait for either tasks to complete or shutdown signal shutdown_task = asyncio.create_task(shutdown_event.wait()) done, pending = await asyncio.wait( [shutdown_task, *tasks], return_when=asyncio.FIRST_COMPLETED ) if shutdown_event.is_set(): # Cancel all pending tasks for task in pending: if not task.done(): task.cancel() # Wait for tasks to finish with timeout (30 seconds grace period) await asyncio.wait_for( asyncio.gather(*pending, return_exceptions=True), timeout=30.0 ) ``` #### Configurazione Pool Database Il pool utilizza `pool_recycle=3600` per riciclare connessioni ogni ora: ```python pool = await aiomysql.create_pool( ... pool_recycle=3600, # Recycle connections every hour ) ``` **Nota**: `aiomysql` non supporta `pool_pre_ping` come SQLAlchemy. La validitΓ  delle connessioni Γ¨ gestita tramite `pool_recycle`. #### Cleanup nel Finally Block ```python finally: if pool: logger.info("Chiusura pool di connessioni database...") pool.close() await pool.wait_closed() logger.info("Pool database chiuso correttamente") logger.info("Shutdown completato") ``` --- ### 3. Worker Loops Tutti e tre gli orchestrator (load, send, elab) sono stati aggiornati. #### Pattern Implementato **Prima**: ```python while True: try: # ... work ... except Exception as e: logger.error(...) ``` **Dopo**: ```python try: while not shutdown_event.is_set(): try: # ... work ... except asyncio.CancelledError: logger.info("Worker cancellato. Uscita in corso...") raise except Exception as e: logger.error(...) except asyncio.CancelledError: logger.info("Worker terminato per shutdown graceful") finally: logger.info("Worker terminato") ``` #### File Modificati 1. **[send_orchestrator.py](src/send_orchestrator.py)** - Importato `shutdown_event` - Worker controlla `shutdown_event.is_set()` nel loop - Gestisce `asyncio.CancelledError` 2. **[load_orchestrator.py](src/load_orchestrator.py)** - Stessa logica di send_orchestrator 3. **[elab_orchestrator.py](src/elab_orchestrator.py)** - Stessa logica di send_orchestrator - Particolare attenzione ai subprocess Matlab che potrebbero essere in esecuzione --- ## πŸ”„ Flusso di Shutdown ``` 1. Sistema riceve SIGTERM/SIGINT ↓ 2. Signal handler setta shutdown_event ↓ 3. run_orchestrator rileva evento shutdown ↓ 4. Cancella tutti i task worker pendenti ↓ 5. Worker ricevono CancelledError ↓ 6. Worker eseguono cleanup nel finally block ↓ 7. Timeout di 30 secondi per completare ↓ 8. Pool database viene chiuso ↓ 9. Applicazione termina pulitamente ``` --- ## ⏱️ Timing e Timeout ### Grace Period: 30 secondi Dopo aver ricevuto il segnale di shutdown, l'applicazione attende fino a 30 secondi per permettere ai worker di terminare le operazioni in corso. ```python await asyncio.wait_for( asyncio.gather(*pending, return_exceptions=True), timeout=30.0 # Grace period for workers to finish ) ``` ### Configurazione per Systemd Se usi systemd, configura il timeout di stop: ```ini [Service] # Attendi 35 secondi prima di forzare il kill (5 secondi in piΓΉ del grace period) TimeoutStopSec=35 ``` ### Configurazione per Docker Se usi Docker, configura il timeout di stop: ```yaml # docker-compose.yml services: ase: stop_grace_period: 35s ``` O con docker run: ```bash docker run --stop-timeout 35 ... ``` --- ## πŸ§ͺ Testing ### Test Manuale #### 1. Test con SIGINT (Ctrl+C) ```bash # Avvia l'orchestrator python src/send_orchestrator.py # Premi Ctrl+C # Dovresti vedere nei log: # - "Ricevuto segnale SIGINT (2). Avvio shutdown graceful..." # - "Shutdown event rilevato. Cancellazione worker in corso..." # - "Worker cancellato. Uscita in corso..." (per ogni worker) # - "Worker terminato per shutdown graceful" (per ogni worker) # - "Chiusura pool di connessioni database..." # - "Shutdown completato" ``` #### 2. Test con SIGTERM ```bash # Avvia l'orchestrator in background python src/send_orchestrator.py & PID=$! # Aspetta che si avvii completamente sleep 5 # Invia SIGTERM kill -TERM $PID # Controlla i log per il graceful shutdown ``` #### 3. Test con Timeout Per testare il timeout di 30 secondi, puoi modificare temporaneamente uno dei worker per simulare un'operazione lunga: ```python # In uno dei worker, aggiungi: if record: logger.info("Simulazione operazione lunga...") await asyncio.sleep(40) # PiΓΉ lungo del grace period # ... ``` Dovresti vedere il warning: ``` "Timeout raggiunto. Alcuni worker potrebbero non essere terminati correttamente" ``` --- ## πŸ“ Log di Esempio ### Shutdown Normale ``` 2025-10-11 10:30:45 - PID: 12345.Worker-W00.root.info: Inizio elaborazione 2025-10-11 10:30:50 - PID: 12345.Worker-^-^.root.info: Ricevuto segnale SIGTERM (15). Avvio shutdown graceful... 2025-10-11 10:30:50 - PID: 12345.Worker-^-^.root.info: Shutdown event rilevato. Cancellazione worker in corso... 2025-10-11 10:30:50 - PID: 12345.Worker-^-^.root.info: In attesa della terminazione di 4 worker... 2025-10-11 10:30:51 - PID: 12345.Worker-W00.root.info: Worker cancellato. Uscita in corso... 2025-10-11 10:30:51 - PID: 12345.Worker-W00.root.info: Worker terminato per shutdown graceful 2025-10-11 10:30:51 - PID: 12345.Worker-W00.root.info: Worker terminato 2025-10-11 10:30:51 - PID: 12345.Worker-W01.root.info: Worker terminato per shutdown graceful 2025-10-11 10:30:51 - PID: 12345.Worker-W02.root.info: Worker terminato per shutdown graceful 2025-10-11 10:30:51 - PID: 12345.Worker-W03.root.info: Worker terminato per shutdown graceful 2025-10-11 10:30:51 - PID: 12345.Worker-^-^.root.info: Tutti i worker terminati correttamente 2025-10-11 10:30:51 - PID: 12345.Worker-^-^.root.info: Chiusura pool di connessioni database... 2025-10-11 10:30:52 - PID: 12345.Worker-^-^.root.info: Pool database chiuso correttamente 2025-10-11 10:30:52 - PID: 12345.Worker-^-^.root.info: Shutdown completato ``` --- ## ⚠️ Note Importanti ### 1. Operazioni Non Interrompibili Alcune operazioni non possono essere interrotte immediatamente: - **Subprocess Matlab**: Continueranno fino al completamento o timeout - **Transazioni Database**: Verranno completate o rollback automatico - **FTP Sincrone**: Bloccheranno fino al completamento (TODO: migrazione a aioftp) ### 2. Perdita di Dati Durante lo shutdown, potrebbero esserci record "locked" nel database se un worker veniva cancellato durante il processamento. Questi record verranno rielaborati al prossimo avvio. ### 3. Signal Handler Limitations I signal handler in Python hanno alcune limitazioni: - Non possono eseguire operazioni async direttamente - Devono essere thread-safe - La nostra implementazione usa semplicemente `shutdown_event.set()` che Γ¨ sicuro ### 4. Nested Event Loops Se usi Jupyter o altri ambienti con event loop nested, il comportamento potrebbe variare. --- ## πŸ” Troubleshooting ### Shutdown Non Completa **Sintomo**: L'applicazione non termina dopo SIGTERM **Possibili cause**: 1. Worker bloccati in operazioni sincrone (FTP, file I/O vecchio) 2. Deadlock nel database 3. Subprocess che non terminano **Soluzione**: - Controlla i log per vedere quali worker non terminano - Verifica operazioni bloccanti con `ps aux | grep python` - Usa SIGKILL solo come ultima risorsa: `kill -9 PID` ### Timeout Raggiunto **Sintomo**: Log mostra "Timeout raggiunto..." **Possibile causa**: Worker impegnati in operazioni lunghe **Soluzione**: - Aumenta il timeout se necessario - Identifica le operazioni lente e ottimizzale - Considera di rendere le operazioni piΓΉ interrompibili ### Database Connection Errors **Sintomo**: Errori di connessione dopo shutdown **Causa**: Pool non chiuso correttamente **Soluzione**: - Verifica che il finally block venga sempre eseguito - Controlla che non ci siano eccezioni non gestite --- ## πŸš€ Deploy ### Systemd Service File ```ini [Unit] Description=ASE Send Orchestrator After=network.target mysql.service [Service] Type=simple User=ase WorkingDirectory=/opt/ase Environment=LOG_LEVEL=INFO ExecStart=/opt/ase/.venv/bin/python /opt/ase/src/send_orchestrator.py Restart=on-failure RestartSec=10 TimeoutStopSec=35 KillMode=mixed [Install] WantedBy=multi-user.target ``` ### Docker Compose ```yaml version: '3.8' services: ase-send: image: ase:latest command: python src/send_orchestrator.py stop_grace_period: 35s stop_signal: SIGTERM environment: - LOG_LEVEL=INFO restart: unless-stopped ``` --- ## βœ… Checklist Post-Implementazione - βœ… Signal handlers configurati per SIGTERM e SIGINT - βœ… shutdown_event implementato e condiviso - βœ… Tutti i worker controllano shutdown_event - βœ… Gestione CancelledError in tutti i worker - βœ… Finally block per cleanup in tutti i worker - βœ… Pool database con pool_pre_ping=True - βœ… Pool database chiuso correttamente nel finally - βœ… Timeout di 30 secondi implementato - βœ… Sintassi Python verificata - ⚠️ Testing manuale da eseguire - ⚠️ Deployment configuration da aggiornare --- ## πŸ“š Riferimenti - [Python asyncio - Signal Handling](https://docs.python.org/3/library/asyncio-eventloop.html#set-signal-handlers-for-sigint-and-sigterm) - [Graceful Shutdown Best Practices](https://cloud.google.com/blog/products/containers-kubernetes/kubernetes-best-practices-terminating-with-grace) - [systemd Service Unit Configuration](https://www.freedesktop.org/software/systemd/man/systemd.service.html) - [Docker Stop Behavior](https://docs.docker.com/engine/reference/commandline/stop/) --- **Autore**: Claude Code **Review**: Da effettuare dal team **Testing**: In attesa di test funzionali