Files
company-ai-platform/app/application/scheduling/scheduler.py
JiuContinent eb8267ed18 ```
feat(feishu): 添加飞书入站事件inbox和混合数据库协调功能

- 实现飞书入站事件持久化inbox机制,支持状态管理、租约锁定和重试退避
- 添加混合数据库基线协调工具,确保平台PostgreSQL结构安全对齐
- 增加运行组件心跳检测和readiness就绪检查机制
- 实现app_ticket事件的安全轮换和验证处理
- 添加生产环境运行编排和fail-closed安全机制
- 支持webhook快速确认和长连接独立进程处理
- 完善个人数据擦除时的待处理事件清理功能
```
2026-07-27 17:14:37 +08:00

408 lines
13 KiB
Python

from collections.abc import Callable
from datetime import UTC, date, datetime
from socket import gethostname
from typing import Any
from fastapi import FastAPI
from app.core.config import get_settings
from app.core.constants import ActorValue
from app.modules.feishu.constants import FeishuReceiveIdType
from app.application.delivery import ReportDeliveryService
from app.modules.observability.constants import HeartbeatComponent
def attach_scheduler(app: FastAPI) -> None:
"""Attach optional APScheduler jobs to the FastAPI application."""
settings = get_settings()
if not settings.scheduler_enabled:
return
scheduler = create_scheduler(app)
@app.on_event("startup")
def start_scheduler() -> None:
scheduler.start()
@app.on_event("shutdown")
def stop_scheduler() -> None:
scheduler.shutdown(wait=False)
def create_scheduler(app: FastAPI | None = None) -> Any:
"""Create the V2/V3 scheduler without requiring a FastAPI process."""
from apscheduler.schedulers.background import BackgroundScheduler
from app.core.database import SessionLocal
from app.core.background.task_queue import (
enqueue_attendance_summary_push,
enqueue_daily_brief_push,
enqueue_event_dispatch,
enqueue_feishu_inbound_cycle,
enqueue_legacy_project_sync,
enqueue_legacy_task_sync,
enqueue_lifecycle_report,
enqueue_market_report,
enqueue_project_weekly_push,
enqueue_risk_progress_push,
enqueue_subscription_cycle,
enqueue_worker_heartbeat,
enqueue_work_daily_push,
enqueue_work_weekly_push,
)
from app.modules.observability.service import ObservabilityService
from app.modules.personalization.services import ConversationService
from app.modules.reports.services import ReportService
settings = get_settings()
scheduler = BackgroundScheduler(
timezone="Asia/Shanghai",
job_defaults={
"coalesce": True,
"max_instances": 1,
"misfire_grace_time": 300,
},
)
def deliver_report(
state_key: str,
build_report: Callable[[Any], dict[str, Any]],
enqueue_push: Callable[..., dict[str, Any]],
) -> None:
db = SessionLocal()
try:
service = ReportService(db)
report = build_report(service)
_set_state(app, state_key, report)
if not _feishu_delivery_configured(settings):
return
if settings.task_queue_enabled:
dispatch = enqueue_push(
receive_id=settings.feishu_default_chat_id,
receive_id_type=FeishuReceiveIdType.CHAT_ID,
actor=ActorValue.SCHEDULER,
)
_set_state(app, f"{state_key}_dispatch", dispatch)
return
ReportDeliveryService(db).push_report(
report,
receive_id=settings.feishu_default_chat_id,
receive_id_type=FeishuReceiveIdType.CHAT_ID,
actor=ActorValue.SCHEDULER,
)
finally:
db.close()
def run_daily_brief() -> None:
deliver_report(
"last_daily_brief",
lambda service: service.daily_brief(),
enqueue_daily_brief_push,
)
def run_project_weekly() -> None:
deliver_report(
"last_project_weekly",
lambda service: service.project_weekly(),
enqueue_project_weekly_push,
)
def run_attendance_summary() -> None:
deliver_report(
"last_attendance_summary",
lambda service: service.attendance_summary(),
enqueue_attendance_summary_push,
)
def run_risk_progress() -> None:
deliver_report(
"last_risk_progress",
lambda service: service.risk_progress(),
enqueue_risk_progress_push,
)
def run_work_daily() -> None:
deliver_report(
"last_work_daily",
lambda service: service.work_daily_report(
reporter=ActorValue.SCHEDULER,
actor=ActorValue.SCHEDULER,
),
enqueue_work_daily_push,
)
def run_work_weekly() -> None:
deliver_report(
"last_work_weekly",
lambda service: service.work_weekly_report(
reporter=ActorValue.SCHEDULER,
actor=ActorValue.SCHEDULER,
),
enqueue_work_weekly_push,
)
def run_legacy_project_sync() -> None:
dispatch = enqueue_legacy_project_sync(actor=ActorValue.SCHEDULER)
_set_state(app, "last_legacy_project_sync_dispatch", dispatch)
def run_legacy_task_sync() -> None:
dispatch = enqueue_legacy_task_sync(actor=ActorValue.SCHEDULER)
_set_state(app, "last_legacy_task_sync_dispatch", dispatch)
def run_daily_lifecycle() -> None:
dispatch = enqueue_lifecycle_report(
report_type="daily",
receive_id=settings.feishu_default_chat_id,
receive_id_type=FeishuReceiveIdType.CHAT_ID,
actor=ActorValue.SCHEDULER,
)
_set_state(app, "last_daily_lifecycle_dispatch", dispatch)
def run_weekly_lifecycle() -> None:
dispatch = enqueue_lifecycle_report(
report_type="weekly",
receive_id=settings.feishu_default_chat_id,
receive_id_type=FeishuReceiveIdType.CHAT_ID,
actor=ActorValue.SCHEDULER,
)
_set_state(app, "last_weekly_lifecycle_dispatch", dispatch)
def run_market_premarket() -> None:
dispatch = enqueue_market_report("premarket")
_set_state(app, "last_market_premarket_dispatch", dispatch)
def run_market_close() -> None:
dispatch = enqueue_market_report("close")
_set_state(app, "last_market_close_dispatch", dispatch)
def run_market_weekly() -> None:
dispatch = enqueue_market_report("weekly", date.today())
_set_state(app, "last_market_weekly_dispatch", dispatch)
def run_event_dispatch() -> None:
dispatch = enqueue_event_dispatch(
limit=settings.event_dispatch_batch_size,
actor=ActorValue.SCHEDULER,
)
_set_state(app, "last_event_dispatch", dispatch)
def run_feishu_inbound_cycle() -> None:
dispatch = enqueue_feishu_inbound_cycle(actor=ActorValue.SCHEDULER)
_set_state(app, "last_feishu_inbound_cycle", dispatch)
def record_scheduler_heartbeat() -> None:
db = SessionLocal()
try:
heartbeat = ObservabilityService(db).record_heartbeat(
component=HeartbeatComponent.SCHEDULER,
instance_id=gethostname(),
actor=ActorValue.SCHEDULER,
)
_set_state(app, "last_scheduler_heartbeat", heartbeat)
finally:
db.close()
def run_subscription_cycle() -> None:
dispatch = enqueue_subscription_cycle(actor=ActorValue.SCHEDULER)
_set_state(app, "last_subscription_cycle", dispatch)
def run_worker_heartbeat() -> None:
dispatch = enqueue_worker_heartbeat(actor=ActorValue.SCHEDULER)
_set_state(app, "last_worker_heartbeat", dispatch)
def run_personalization_retention_cleanup() -> None:
db = SessionLocal()
try:
deleted = ConversationService(db).cleanup_expired_globally()
_set_state(app, "last_personalization_retention_cleanup", deleted)
finally:
db.close()
if settings.lifecycle_pipeline_enabled:
scheduler.add_job(
run_daily_lifecycle,
trigger="cron",
hour=settings.daily_brief_cron_hour,
minute=settings.daily_brief_cron_minute,
id="daily_lifecycle_pipeline",
replace_existing=True,
)
scheduler.add_job(
run_weekly_lifecycle,
trigger="cron",
day_of_week=settings.weekly_project_report_day_of_week,
hour=settings.weekly_project_report_cron_hour,
minute=settings.weekly_project_report_cron_minute,
id="weekly_lifecycle_pipeline",
replace_existing=True,
)
else:
scheduler.add_job(
run_daily_brief,
trigger="cron",
hour=settings.daily_brief_cron_hour,
minute=settings.daily_brief_cron_minute,
id="daily_brief_push",
replace_existing=True,
)
scheduler.add_job(
run_project_weekly,
trigger="cron",
day_of_week=settings.weekly_project_report_day_of_week,
hour=settings.weekly_project_report_cron_hour,
minute=settings.weekly_project_report_cron_minute,
id="project_weekly_push",
replace_existing=True,
)
scheduler.add_job(
run_attendance_summary,
trigger="cron",
hour=settings.attendance_summary_cron_hour,
minute=settings.attendance_summary_cron_minute,
id="attendance_summary_push",
replace_existing=True,
)
scheduler.add_job(
run_risk_progress,
trigger="cron",
hour=settings.risk_progress_cron_hour,
minute=settings.risk_progress_cron_minute,
id="risk_progress_push",
replace_existing=True,
)
scheduler.add_job(
run_work_daily,
trigger="cron",
hour=settings.work_daily_cron_hour,
minute=settings.work_daily_cron_minute,
id="work_daily_push",
replace_existing=True,
)
scheduler.add_job(
run_work_weekly,
trigger="cron",
day_of_week=settings.work_weekly_report_day_of_week,
hour=settings.work_weekly_cron_hour,
minute=settings.work_weekly_cron_minute,
id="work_weekly_push",
replace_existing=True,
)
if settings.event_dispatch_enabled:
scheduler.add_job(
run_event_dispatch,
trigger="cron",
minute=settings.event_dispatch_cron_minute,
id="event_dispatch",
next_run_time=datetime.now(UTC),
replace_existing=True,
)
if settings.market_analysis_enabled:
scheduler.add_job(
run_market_premarket,
trigger="cron",
day_of_week="mon-fri",
hour=settings.market_premarket_cron_hour,
minute=settings.market_premarket_cron_minute,
id="market_premarket_analysis",
replace_existing=True,
)
scheduler.add_job(
run_market_close,
trigger="cron",
day_of_week="mon-fri",
hour=settings.market_close_cron_hour,
minute=settings.market_close_cron_minute,
id="market_close_analysis",
replace_existing=True,
)
scheduler.add_job(
run_market_weekly,
trigger="cron",
day_of_week=settings.market_weekly_day_of_week,
hour=settings.market_weekly_cron_hour,
minute=settings.market_weekly_cron_minute,
id="market_weekly_analysis",
replace_existing=True,
)
scheduler.add_job(
record_scheduler_heartbeat,
trigger="interval",
seconds=settings.heartbeat_interval_seconds,
id="scheduler_heartbeat",
next_run_time=datetime.now(UTC),
replace_existing=True,
)
if settings.task_queue_enabled:
scheduler.add_job(
run_worker_heartbeat,
trigger="interval",
seconds=settings.heartbeat_interval_seconds,
id="worker_heartbeat",
next_run_time=datetime.now(UTC),
replace_existing=True,
)
scheduler.add_job(
run_feishu_inbound_cycle,
trigger="interval",
minutes=1,
id="feishu_inbound_event_cycle",
replace_existing=True,
)
if settings.feishu_user_features_enabled:
scheduler.add_job(
run_subscription_cycle,
trigger="interval",
minutes=1,
id="subscription_delivery_cycle",
replace_existing=True,
)
scheduler.add_job(
run_personalization_retention_cleanup,
trigger="interval",
minutes=1,
id="personalization_retention_cleanup",
replace_existing=True,
)
if (
not settings.lifecycle_pipeline_enabled
and settings.legacy_sync_enabled
and settings.legacy_project_query
):
scheduler.add_job(
run_legacy_project_sync,
trigger="cron",
hour=settings.legacy_project_sync_cron_hour,
minute=settings.legacy_project_sync_cron_minute,
id="legacy_project_sync",
replace_existing=True,
)
if (
not settings.lifecycle_pipeline_enabled
and settings.legacy_sync_enabled
and settings.legacy_task_query
):
scheduler.add_job(
run_legacy_task_sync,
trigger="cron",
hour=settings.legacy_task_sync_cron_hour,
minute=settings.legacy_task_sync_cron_minute,
id="legacy_task_sync",
replace_existing=True,
)
return scheduler
def _set_state(app: FastAPI | None, key: str, value: Any) -> None:
if app is not None:
setattr(app.state, key, value)
def _feishu_delivery_configured(settings: Any) -> bool:
return bool(
settings.feishu_app_id
and settings.feishu_app_secret
and settings.feishu_default_chat_id
)