import asyncio import logging from database import SessionLocal from models import VitalReading, TransmissionLog from schemas import NormalizedVitals from api_client import forward_vitals_to_api from config import settings logger = logging.getLogger(__name__) async def retry_failed_transmissions(): """ Background task that periodically checks for untransmitted vital readings and attempts to forward them to the API. """ logger.info("Starting background task for retrying failed transmissions...") while True: try: # 1. Fetch untransmitted readings in a quick DB session and close it immediately db = SessionLocal() readings_to_retry = [] try: failed_readings = db.query(VitalReading).filter(VitalReading.transmitted == False).order_by(VitalReading.id.desc()).limit(20).all() for r in failed_readings: readings_to_retry.append({ "id": r.id, "vitals": NormalizedVitals( device_id=r.device_id, patient_id=r.patient_id, timestamp=r.timestamp, heart_rate=r.heart_rate, spo2=r.spo2, systolic_bp=r.systolic_bp, diastolic_bp=r.diastolic_bp, map_bp=r.map_bp, respiratory_rate=r.respiratory_rate, temperature=r.temperature ) }) finally: db.close() # 2. Forward outside of any DB transaction successful_ids = [] failed_ids = [] consecutive_failures = 0 for item in readings_to_retry: success = await forward_vitals_to_api(item["vitals"]) if success: successful_ids.append(item["id"]) consecutive_failures = 0 else: failed_ids.append(item["id"]) consecutive_failures += 1 # If external API is unresponsive, stop this batch immediately so we don't spam if consecutive_failures >= 3: logger.warning("External API appears unreachable during retry; pausing retry queue.") break # 3. Update status in a quick DB session if successful_ids or failed_ids: db = SessionLocal() try: for rid in successful_ids: db.query(VitalReading).filter(VitalReading.id == rid).update({"transmitted": True}) db.add(TransmissionLog(reading_id=rid, status="success", response_code=200)) for rid in failed_ids: db.add(TransmissionLog(reading_id=rid, status="failed", error_message="Retry failed")) db.commit() except Exception as e: logger.error(f"Failed updating retry transmission status: {e}") db.rollback() finally: db.close() except Exception as e: logger.error(f"Error in retry background task: {e}") await asyncio.sleep(settings.retry_interval_seconds) async def sync_cloud_state(): """ Periodically syncs all assigned patients and their latest non-null vitals to Render Cloud. Ensures that whenever Render restarts or deploys with an empty DB, the cloud dashboard is immediately populated with the same data as localhost. """ import os if os.getenv("RENDER") is not None or os.getenv("PORT") == "10000" or "render.local" in os.getenv("HOSTNAME", ""): return # Do not run sync loop if we are running inside Render from models import Patient from api_client import sync_patient_assignment_to_cloud, forward_vitals_to_api from datetime import datetime, timezone from sqlalchemy import or_ logger.info("Starting background task for periodic cloud state sync to Render...") await asyncio.sleep(5) while True: try: db = SessionLocal() try: patients = db.query(Patient).filter( Patient.patient_id != "UNKNOWN", Patient.mrn != "UNKNOWN" ).all() for p in patients: if not p.device_id: continue # 1. Sync patient demographics assignment to Render p_data = { "mrn": p.mrn or p.patient_id, "name": p.name, "gender": p.gender, "dob": p.dob, "bed_id": p.bed_id, "patient_type": getattr(p, "patient_type", None) } await sync_patient_assignment_to_cloud(p_data, p.device_id) # 2. Backfill and forward latest complete non-null vitals to Render patient_filters = or_( VitalReading.patient_id == p.mrn, VitalReading.patient_id == p.patient_id, VitalReading.device_id == p.device_id ) recent_r = db.query(VitalReading).filter(patient_filters).order_by(VitalReading.timestamp.desc()).first() if recent_r: spo2_r = db.query(VitalReading.spo2).filter(patient_filters, VitalReading.spo2 != None).order_by(VitalReading.timestamp.desc()).first() hr_r = db.query(VitalReading.heart_rate).filter(patient_filters, VitalReading.heart_rate != None).order_by(VitalReading.timestamp.desc()).first() bp_r = db.query(VitalReading.systolic_bp, VitalReading.diastolic_bp, VitalReading.map_bp).filter(patient_filters, VitalReading.systolic_bp != None).order_by(VitalReading.timestamp.desc()).first() temp_r = db.query(VitalReading.temperature).filter(patient_filters, VitalReading.temperature != None).order_by(VitalReading.timestamp.desc()).first() rr_r = db.query(VitalReading.respiratory_rate).filter(patient_filters, VitalReading.respiratory_rate != None).order_by(VitalReading.timestamp.desc()).first() merged = NormalizedVitals( device_id=p.device_id, patient_id=p.mrn or p.patient_id, patient_name=p.name, patient_gender=p.gender, patient_dob=p.dob, bed_id=p.bed_id, patient_type=getattr(p, "patient_type", None), timestamp=recent_r.timestamp or datetime.now(timezone.utc), heart_rate=hr_r[0] if hr_r else None, spo2=spo2_r[0] if spo2_r else None, systolic_bp=bp_r[0] if bp_r else None, diastolic_bp=bp_r[1] if bp_r else None, map_bp=bp_r[2] if bp_r else None, temperature=temp_r[0] if temp_r else None, respiratory_rate=rr_r[0] if rr_r else None ) await forward_vitals_to_api(merged) finally: db.close() except Exception as e: logger.error(f"Error in sync_cloud_state background task: {e}") await asyncio.sleep(15)