초기 커밋
This commit is contained in:
0
app/tasks/__init__.py
Normal file
0
app/tasks/__init__.py
Normal file
56
app/tasks/analytics_tasks.py
Normal file
56
app/tasks/analytics_tasks.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import structlog
|
||||
|
||||
from app.tasks.celery_app import celery_app
|
||||
|
||||
logger = structlog.get_logger("tasks.analytics")
|
||||
|
||||
|
||||
@celery_app.task(name="app.tasks.analytics_tasks.run_daily_analytics")
|
||||
def run_daily_analytics() -> dict:
|
||||
"""Run daily aggregation analytics on telemetry data."""
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.models.mongodb.analytics_result import AnalyticsResult
|
||||
from app.models.mongodb.telemetry import TelemetryData
|
||||
|
||||
async def _run() -> dict:
|
||||
yesterday = datetime.utcnow().replace(hour=0, minute=0, second=0) - timedelta(days=1)
|
||||
today = yesterday + timedelta(days=1)
|
||||
|
||||
pipeline = [
|
||||
{"$match": {"timestamp": {"$gte": yesterday, "$lt": today}}},
|
||||
{"$group": {
|
||||
"_id": "$device_id",
|
||||
"count": {"$sum": 1},
|
||||
"avg_metrics": {"$avg": "$metrics.value"},
|
||||
}},
|
||||
]
|
||||
|
||||
collection = TelemetryData.get_motor_collection()
|
||||
results = await collection.aggregate(pipeline).to_list(length=1000)
|
||||
|
||||
for r in results:
|
||||
await AnalyticsResult(
|
||||
analysis_type="daily_telemetry",
|
||||
device_id=r["_id"],
|
||||
parameters={"date": yesterday.isoformat()},
|
||||
result={"count": r["count"], "avg_value": r.get("avg_metrics")},
|
||||
period_start=yesterday,
|
||||
period_end=today,
|
||||
).insert()
|
||||
|
||||
return {"devices_analyzed": len(results)}
|
||||
|
||||
result = asyncio.get_event_loop().run_until_complete(_run())
|
||||
logger.info("daily_analytics_done", **result)
|
||||
return result
|
||||
|
||||
|
||||
@celery_app.task(name="app.tasks.analytics_tasks.run_device_analysis")
|
||||
def run_device_analysis(device_id: str, analysis_type: str, params: dict) -> dict:
|
||||
"""Run on-demand analysis for a specific device."""
|
||||
logger.info("device_analysis_started", device_id=device_id, type=analysis_type)
|
||||
return {"status": "completed", "device_id": device_id, "type": analysis_type}
|
||||
47
app/tasks/auth_tasks.py
Normal file
47
app/tasks/auth_tasks.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import structlog
|
||||
|
||||
from app.tasks.celery_app import celery_app
|
||||
|
||||
logger = structlog.get_logger("tasks.auth")
|
||||
|
||||
|
||||
@celery_app.task(name="app.tasks.auth_tasks.cleanup_expired_tokens")
|
||||
def cleanup_expired_tokens() -> dict:
|
||||
"""Remove expired and revoked refresh tokens from the database."""
|
||||
import asyncio
|
||||
|
||||
from sqlalchemy import delete
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from datetime import datetime
|
||||
|
||||
from app.core.config import settings
|
||||
from app.models.mariadb.auth import RefreshToken
|
||||
|
||||
async def _cleanup() -> int:
|
||||
engine = create_async_engine(settings.MARIADB_DSN)
|
||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async with async_session() as session:
|
||||
stmt = delete(RefreshToken).where(
|
||||
(RefreshToken.expires_at < datetime.utcnow()) | (RefreshToken.is_revoked == True) # noqa: E712
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
await session.commit()
|
||||
count = result.rowcount # type: ignore[assignment]
|
||||
|
||||
await engine.dispose()
|
||||
return count
|
||||
|
||||
count = asyncio.get_event_loop().run_until_complete(_cleanup())
|
||||
logger.info("tokens_cleaned", count=count)
|
||||
return {"cleaned": count}
|
||||
|
||||
|
||||
@celery_app.task(name="app.tasks.auth_tasks.send_verification_email")
|
||||
def send_verification_email(user_id: int, email: str, token: str) -> dict:
|
||||
"""Send email verification link to user."""
|
||||
logger.info("verification_email_sent", user_id=user_id, email=email)
|
||||
return {"status": "sent", "email": email}
|
||||
49
app/tasks/celery_app.py
Normal file
49
app/tasks/celery_app.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from celery import Celery
|
||||
from celery.schedules import crontab
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
celery_app = Celery(
|
||||
"core_api",
|
||||
broker=settings.CELERY_BROKER_URL,
|
||||
backend=settings.CELERY_RESULT_BACKEND,
|
||||
)
|
||||
|
||||
celery_app.conf.update(
|
||||
task_serializer="json",
|
||||
accept_content=["json"],
|
||||
result_serializer="json",
|
||||
timezone="Asia/Seoul",
|
||||
enable_utc=True,
|
||||
task_track_started=True,
|
||||
task_routes={
|
||||
"app.tasks.analytics_tasks.*": {"queue": "analytics"},
|
||||
"app.tasks.notification_tasks.*": {"queue": "notifications"},
|
||||
"app.tasks.device_tasks.*": {"queue": "devices"},
|
||||
"app.tasks.auth_tasks.*": {"queue": "default"},
|
||||
},
|
||||
beat_schedule={
|
||||
"cleanup-expired-tokens": {
|
||||
"task": "app.tasks.auth_tasks.cleanup_expired_tokens",
|
||||
"schedule": crontab(hour=3, minute=0),
|
||||
},
|
||||
"check-device-health": {
|
||||
"task": "app.tasks.device_tasks.check_device_health",
|
||||
"schedule": crontab(minute="*/5"),
|
||||
},
|
||||
"daily-analytics": {
|
||||
"task": "app.tasks.analytics_tasks.run_daily_analytics",
|
||||
"schedule": crontab(hour=1, minute=0),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
celery_app.autodiscover_tasks([
|
||||
"app.tasks.auth_tasks",
|
||||
"app.tasks.device_tasks",
|
||||
"app.tasks.notification_tasks",
|
||||
"app.tasks.analytics_tasks",
|
||||
"app.tasks.scheduled",
|
||||
])
|
||||
64
app/tasks/device_tasks.py
Normal file
64
app/tasks/device_tasks.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import structlog
|
||||
|
||||
from app.tasks.celery_app import celery_app
|
||||
|
||||
logger = structlog.get_logger("tasks.device")
|
||||
|
||||
|
||||
@celery_app.task(name="app.tasks.device_tasks.check_device_health")
|
||||
def check_device_health() -> dict:
|
||||
"""Check all devices for heartbeat timeout and mark offline."""
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.constants import DeviceStatus
|
||||
from app.models.mariadb.device import Device
|
||||
|
||||
async def _check() -> int:
|
||||
engine = create_async_engine(settings.MARIADB_DSN)
|
||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
threshold = datetime.utcnow() - timedelta(minutes=10)
|
||||
|
||||
async with async_session() as session:
|
||||
stmt = (
|
||||
update(Device)
|
||||
.where(
|
||||
Device.status == DeviceStatus.ONLINE,
|
||||
Device.last_seen_at < threshold,
|
||||
Device.is_deleted == False, # noqa: E712
|
||||
)
|
||||
.values(status=DeviceStatus.OFFLINE)
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
await session.commit()
|
||||
count = result.rowcount # type: ignore[assignment]
|
||||
|
||||
await engine.dispose()
|
||||
return count
|
||||
|
||||
count = asyncio.get_event_loop().run_until_complete(_check())
|
||||
logger.info("device_health_check", offline_count=count)
|
||||
return {"marked_offline": count}
|
||||
|
||||
|
||||
@celery_app.task(name="app.tasks.device_tasks.batch_firmware_update")
|
||||
def batch_firmware_update(device_uids: list[str], firmware_url: str) -> dict:
|
||||
"""Trigger OTA firmware update for a batch of devices."""
|
||||
import asyncio
|
||||
from app.communication.mqtt.publisher import publish_ota
|
||||
|
||||
async def _update() -> int:
|
||||
for uid in device_uids:
|
||||
await publish_ota(uid, {"url": firmware_url, "action": "update"})
|
||||
return len(device_uids)
|
||||
|
||||
count = asyncio.get_event_loop().run_until_complete(_update())
|
||||
logger.info("batch_firmware_update", count=count)
|
||||
return {"updated": count}
|
||||
47
app/tasks/notification_tasks.py
Normal file
47
app/tasks/notification_tasks.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import structlog
|
||||
|
||||
from app.tasks.celery_app import celery_app
|
||||
|
||||
logger = structlog.get_logger("tasks.notification")
|
||||
|
||||
|
||||
@celery_app.task(name="app.tasks.notification_tasks.send_push_notification")
|
||||
def send_push_notification(user_id: int, title: str, message: str) -> dict:
|
||||
"""Send push notification to a user via Socket.IO."""
|
||||
import asyncio
|
||||
|
||||
from app.communication.socketio.server import sio
|
||||
|
||||
async def _send() -> None:
|
||||
await sio.emit(
|
||||
"notification",
|
||||
{"title": title, "message": message},
|
||||
room=f"user:{user_id}",
|
||||
namespace="/notification",
|
||||
)
|
||||
|
||||
asyncio.get_event_loop().run_until_complete(_send())
|
||||
logger.info("push_sent", user_id=user_id)
|
||||
return {"status": "sent", "user_id": user_id}
|
||||
|
||||
|
||||
@celery_app.task(name="app.tasks.notification_tasks.send_bulk_notification")
|
||||
def send_bulk_notification(user_ids: list[int], title: str, message: str) -> dict:
|
||||
"""Send notification to multiple users and store in MongoDB."""
|
||||
import asyncio
|
||||
|
||||
from app.models.mongodb.notification import Notification
|
||||
|
||||
async def _bulk() -> int:
|
||||
notifications = [
|
||||
Notification(user_id=uid, title=title, message=message)
|
||||
for uid in user_ids
|
||||
]
|
||||
await Notification.insert_many(notifications)
|
||||
return len(notifications)
|
||||
|
||||
count = asyncio.get_event_loop().run_until_complete(_bulk())
|
||||
logger.info("bulk_notification_sent", count=count)
|
||||
return {"sent": count}
|
||||
14
app/tasks/scheduled.py
Normal file
14
app/tasks/scheduled.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import structlog
|
||||
|
||||
from app.tasks.celery_app import celery_app
|
||||
|
||||
logger = structlog.get_logger("tasks.scheduled")
|
||||
|
||||
|
||||
@celery_app.task(name="app.tasks.scheduled.system_health_report")
|
||||
def system_health_report() -> dict:
|
||||
"""Generate periodic system health report."""
|
||||
logger.info("system_health_report_generated")
|
||||
return {"status": "ok"}
|
||||
Reference in New Issue
Block a user