G41_TAPD_BUG_SYNC/src2/scheduler.py
2026-03-10 20:53:32 +08:00

420 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
任务二调度器模块
负责定时执行TAPD状态同步任务
功能:
1. 按配置频率定时执行
2. 优雅退出Ctrl+C
3. 运行统计
"""
import sys
import time
import signal
import argparse
from pathlib import Path
from datetime import datetime
# 将项目根目录添加到 Python 路径
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
# 导入schedule库
try:
import schedule
except ImportError:
print("错误: 缺少schedule库")
print("请运行: pip install schedule")
sys.exit(1)
from src.token_manager import TokenManager
from src2.config import Task2ConfigManager
from src2.sync_service import run_once
from src2.logger import get_task2_logger
class Task2Scheduler:
"""任务二调度器"""
def __init__(self, config_path=None, verbose=False):
"""
初始化调度器
Args:
config_path: 配置文件路径
verbose: 是否显示详细信息
"""
self.config_path = config_path
self.verbose = verbose
self.running = True
# 统计信息
self.stats = {
'start_time': None,
'total_runs': 0,
'success_runs': 0,
'failed_runs': 0,
'total_records_synced': 0,
'total_records_updated': 0,
'last_run_time': None
}
# 初始化配置管理器
self._init_config()
# 初始化TokenManager
self._init_token_manager()
# 获取调度配置
self.sync_interval = self.config['schedule']['sync_interval']
def _init_config(self):
"""初始化配置"""
try:
print("正在加载配置文件...")
self.config_manager = Task2ConfigManager(config_path=self.config_path)
self.config = self.config_manager.get_all_config()
print("✓ 配置文件加载成功")
except Exception as e:
print(f"✗ 配置文件加载失败: {e}")
raise
def _init_token_manager(self):
"""初始化TokenManager"""
try:
print("正在初始化TokenManager...")
self.token_manager = TokenManager(logger=get_task2_logger())
print("✓ TokenManager初始化成功")
except Exception as e:
print(f"✗ TokenManager初始化失败: {e}")
raise
def job(self):
"""执行一次同步任务"""
logger = get_task2_logger()
logger.start_sync(
trigger="task2_scheduler_job",
metadata={
"entry": "src2/scheduler.py:Task2Scheduler.job",
"verbose": self.verbose,
}
)
result = {
"success": False,
"docs_total": 0,
"docs_success": 0,
"docs_failed": 0,
"sheets_processed": 0,
"sheets_skipped": 0,
"total_records": 0,
"records_with_link": 0,
"records_synced": 0,
"records_updated": 0,
"records_failed": 0,
"error_message": None,
}
print("\n" + "=" * 80)
print(f"开始执行同步任务 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("=" * 80)
try:
# 获取access_token
access_token = self.token_manager.get_token()
# 执行一次同步流程
result = run_once(
config_manager=self.config_manager,
access_token=access_token,
test_mode=self.verbose
)
# 更新统计信息
self.stats['total_runs'] += 1
self.stats['last_run_time'] = datetime.now()
if result['success']:
self.stats['success_runs'] += 1
self.stats['total_records_synced'] += result['records_synced']
self.stats['total_records_updated'] += result['records_updated']
print("\n" + "-" * 80)
print("本次执行统计:")
print(f" 处理子表: {result['sheets_processed']}")
print(f" 跳过子表: {result['sheets_skipped']}")
print(f" 包含链接: {result['records_with_link']}")
print(f" 同步成功: {result['records_synced']}")
print(f" 需要更新: {result['records_updated']}")
print(f" 同步失败: {result['records_failed']}")
print("-" * 80)
else:
self.stats['failed_runs'] += 1
print("\n" + "-" * 80)
print("本次执行失败:")
print(f" 错误信息: {result.get('error_message', '未知错误')}")
print("-" * 80)
logger.end_sync_with_stats(
stats={
"docs_total": result.get("docs_total", 0),
"docs_success": result.get("docs_success", 0),
"docs_failed": result.get("docs_failed", 0),
"sheets_processed": result.get("sheets_processed", 0),
"sheets_skipped": result.get("sheets_skipped", 0),
"total_records": result.get("total_records", 0),
"records_with_link": result.get("records_with_link", 0),
"records_synced": result.get("records_synced", 0),
"records_updated": result.get("records_updated", 0),
"records_failed": result.get("records_failed", 0),
},
success=result.get("success", False),
error_message=result.get("error_message"),
extra={"source": "task2_scheduler_job"},
)
except Exception as e:
self.stats['total_runs'] += 1
self.stats['failed_runs'] += 1
self.stats['last_run_time'] = datetime.now()
print("\n" + "-" * 80)
print("本次执行异常:")
print(f" 错误类型: {type(e).__name__}")
print(f" 错误信息: {e}")
print("-" * 80)
if self.verbose:
import traceback
print("\n详细错误信息:")
traceback.print_exc()
if logger.get_active_sync_id():
logger.end_sync_with_stats(
stats={
"docs_total": result.get("docs_total", 0),
"docs_success": result.get("docs_success", 0),
"docs_failed": result.get("docs_failed", 0),
"sheets_processed": result.get("sheets_processed", 0),
"sheets_skipped": result.get("sheets_skipped", 0),
"total_records": result.get("total_records", 0),
"records_with_link": result.get("records_with_link", 0),
"records_synced": result.get("records_synced", 0),
"records_updated": result.get("records_updated", 0),
"records_failed": result.get("records_failed", 0),
},
success=False,
error_message=str(e),
extra={
"source": "task2_scheduler_job",
"exception_type": type(e).__name__,
},
)
# 显示下次执行时间
self._show_next_run_time()
def _show_next_run_time(self):
"""显示下次执行时间"""
next_run = schedule.idle_seconds()
if next_run is not None:
next_run_time = datetime.now().timestamp() + next_run
next_run_str = datetime.fromtimestamp(next_run_time).strftime('%Y-%m-%d %H:%M:%S')
print(f"\n下次执行时间: {next_run_str} (约 {int(next_run / 60)} 分钟后)")
def _startup_check(self):
"""启动检查"""
print("\n" + "=" * 80)
print("启动检查")
print("=" * 80)
checks_passed = True
# 1. 检查配置文件
print("\n[1/3] 检查配置文件...")
try:
print(f" ✓ 配置文件已加载: {self.config_path or '默认路径'}")
print(f" ✓ TAPD workspace_id: {self.config['tapd']['workspace_id']}")
print(f" ✓ SmartSheet docid: {self.config['smartsheet']['docid'][:20]}...")
print(f" ✓ 同步间隔: {self.sync_interval} 分钟")
except Exception as e:
print(f" ✗ 配置检查失败: {e}")
checks_passed = False
# 2. 检查环境变量
print("\n[2/3] 检查环境变量...")
import os
required_env_vars = {
'CORPID': '企业微信CorpID',
'CORPSECRET': '企业微信CorpSecret',
'TAPD_API_USER': 'TAPD API用户名',
'TAPD_API_PASSWORD': 'TAPD API密码'
}
for env_var, description in required_env_vars.items():
if os.environ.get(env_var):
print(f"{description} ({env_var}): 已设置")
else:
print(f"{description} ({env_var}): 未设置")
checks_passed = False
# 3. 测试access_token获取
print("\n[3/3] 测试access_token获取...")
try:
access_token = self.token_manager.get_token()
print(f" ✓ access_token获取成功: {access_token[:10]}...(已隐藏)")
except Exception as e:
print(f" ✗ access_token获取失败: {e}")
checks_passed = False
print("\n" + "=" * 80)
if checks_passed:
print("启动检查通过 ✓")
else:
print("启动检查失败 ✗")
print("\n请检查上述错误并修复后重试")
return False
print("=" * 80)
return True
def start(self):
"""启动调度器"""
print("\n" + "=" * 80)
print("TAPD状态同步调度器 (任务二)")
print("版本: 1.0.0 (第四阶段)")
print("=" * 80)
# 执行启动检查
if not self._startup_check():
print("\n调度器启动失败")
sys.exit(1)
# 记录启动时间
self.stats['start_time'] = datetime.now()
# 注册信号处理
signal.signal(signal.SIGINT, self._signal_handler)
signal.signal(signal.SIGTERM, self._signal_handler)
# 配置定时任务
schedule.every(self.sync_interval).minutes.do(self.job)
print(f"\n调度器已启动:")
print(f" - 同步任务: 立即执行一次,然后每 {self.sync_interval} 分钟执行一次")
print("按 Ctrl+C 停止调度器")
print()
# 立即执行一次同步任务
self.job()
# 进入调度循环
while self.running:
schedule.run_pending()
time.sleep(1)
def _signal_handler(self, signum, frame):
"""信号处理函数"""
print("\n\n" + "=" * 80)
print("收到停止信号,正在优雅退出...")
print("=" * 80)
self.running = False
self._print_final_stats()
def _print_final_stats(self):
"""打印最终统计信息"""
print("\n" + "=" * 80)
print("运行统计")
print("=" * 80)
if self.stats['start_time']:
start_time_str = self.stats['start_time'].strftime('%Y-%m-%d %H:%M:%S')
print(f"启动时间: {start_time_str}")
if self.stats['last_run_time']:
last_run_str = self.stats['last_run_time'].strftime('%Y-%m-%d %H:%M:%S')
print(f"最后执行: {last_run_str}")
if self.stats['start_time']:
running_time = datetime.now() - self.stats['start_time']
hours = int(running_time.total_seconds() // 3600)
minutes = int((running_time.total_seconds() % 3600) // 60)
print(f"运行时长: {hours} 小时 {minutes} 分钟")
print(f"\n同步任务统计:")
print(f" 总执行次数: {self.stats['total_runs']}")
print(f" 成功次数: {self.stats['success_runs']}")
print(f" 失败次数: {self.stats['failed_runs']}")
print(f" 总同步记录: {self.stats['total_records_synced']}")
print(f" 总更新记录: {self.stats['total_records_updated']}")
if self.stats['total_runs'] > 0:
success_rate = (self.stats['success_runs'] / self.stats['total_runs']) * 100
print(f" 成功率: {success_rate:.1f}%")
print("=" * 80)
print("调度器已停止")
print("=" * 80)
def parse_arguments():
"""解析命令行参数"""
parser = argparse.ArgumentParser(
description='TAPD状态同步调度器 - 定时执行同步任务',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例用法:
# 使用默认配置启动调度器
python src2/scheduler.py
# 指定配置文件路径
python src2/scheduler.py --config /path/to/config.ini
# 显示详细信息
python src2/scheduler.py --verbose
"""
)
parser.add_argument(
'-c', '--config',
default=None,
help='配置文件路径(默认: config/config_task2.ini'
)
parser.add_argument(
'-v', '--verbose',
action='store_true',
help='显示详细输出信息'
)
return parser.parse_args()
def main():
"""主函数"""
try:
# 解析命令行参数
args = parse_arguments()
# 创建并启动调度器
scheduler = Task2Scheduler(
config_path=args.config,
verbose=args.verbose
)
scheduler.start()
return 0
except KeyboardInterrupt:
return 0
except Exception as e:
print(f"\n✗ 调度器启动失败: {e}")
import traceback
traceback.print_exc()
return 1
if __name__ == "__main__":
sys.exit(main())