170 lines
8.1 KiB
Python
170 lines
8.1 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. Forward latest non-null vitals to Render (Option B)
|
|
from sqlalchemy import or_
|
|
from datetime import datetime, timezone
|
|
patient_filters = or_(
|
|
VitalReading.patient_id == p.mrn,
|
|
VitalReading.patient_id == p.patient_id,
|
|
VitalReading.device_id == p.device_id
|
|
)
|
|
hr_r = db.query(VitalReading.heart_rate).filter(patient_filters, VitalReading.heart_rate != None).order_by(VitalReading.timestamp.desc()).first()
|
|
spo2_r = db.query(VitalReading.spo2).filter(patient_filters, VitalReading.spo2 != 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()
|
|
temp2_r = db.query(VitalReading.temperature2).filter(patient_filters, VitalReading.temperature2 != None).order_by(VitalReading.timestamp.desc()).first()
|
|
temp_diff_r = db.query(VitalReading.temperature_diff).filter(patient_filters, VitalReading.temperature_diff != 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()
|
|
|
|
if hr_r or spo2_r or temp_r or bp_r:
|
|
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=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,
|
|
temperature2=temp2_r[0] if temp2_r else None,
|
|
temperature_diff=temp_diff_r[0] if temp_diff_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)
|
|
|