Files
ContecMonitor/fiveparaminte-main/background_tasks.py
T

165 lines
7.6 KiB
Python

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)