Celery Soft Time Limit未触发问题:Scrapy任务与sleep测试的差异解析
我创建了一个Celery任务,设置soft_time_limit为10秒,硬限制time_limit为32秒:
from celery.exceptions import SoftTimeLimitExceeded from scrapy.crawler import CrawlerProcess from scrapy.utils.project import get_project_settings import sys @app.task(bind=True, acks_late=False, time_limit=32, soft_time_limit=10) def my_task(self, **kwargs): try: if 'twisted.internet.reactor' in sys.modules: del sys.modules['twisted.internet.reactor'] settings = get_project_settings() process = CrawlerProcess(settings) process.crawl(**kwargs) process.start() except SoftTimeLimitExceeded as te: print('Time Exceeded...')
该代码可正常执行,但当爬取任务耗时较长达到软限制时,SoftTimeLimitExceeded异常未被触发,爬取仍继续,直至达到硬限制时抛出如下错误:
Traceback (most recent call last): File "/usr/local/lib/python3.8/site-packages/billiard/pool.py", line 684, in on_hard_timeout raise TimeLimitExceeded(job._timeout) billiard.exceptions.TimeLimitExceeded: TimeLimitExceeded(32,)
我无法在任务内部捕获该硬限制错误。作为测试,我将process.start()替换为time.sleep(50)以模拟长时间延迟:
@app.task(bind=True, acks_late=False, time_limit=32, soft_time_limit=10) def my_task(self, **kwargs): try: if 'twisted.internet.reactor' in sys.modules: del sys.modules['twisted.internet.reactor'] settings = get_project_settings() process = CrawlerProcess(settings) process.crawl(**kwargs) time.sleep(50) except SoftTimeLimitExceeded as te: print('Time Exceeded...')
此时SoftTimeLimitExceeded异常可被正常捕获。请问出现这一差异的原因是什么?
版本信息:celery5.2.7,Scrapy2.6.1
核心差异在于Twisted Reactor的事件循环接管了进程的信号处理机制,与Celery的时间限制实现逻辑冲突:
Celery时间限制的实现原理
Celery的soft_time_limit和time_limit依赖操作系统信号工作:- 软限制超时后,Celery向任务进程发送
SIGUSR1信号,触发任务线程抛出SoftTimeLimitExceeded异常 - 硬限制超时后,发送
SIGTERM信号,由billiard(Celery的子进程管理库)的监控线程直接终止进程并抛出TimeLimitExceeded
这两种信号都需要进程的默认信号处理机制正常运行,才能在任务线程中被捕获或响应。
- 软限制超时后,Celery向任务进程发送
Twisted Reactor对信号的接管
调用process.start()时,Scrapy基于Twisted的reactor.run()启动事件循环,Twisted会覆盖进程的默认信号处理器,将信号转化为事件循环内部的事件。此时Celery发送的SIGUSR1信号会被Twisted捕获,但不会转发给Celery的信号处理器,因此无法触发SoftTimeLimitExceeded异常。而硬限制的SIGTERM信号会直接终止进程,对应的TimeLimitExceeded异常是在billiard的监控线程中抛出的,不属于任务执行线程的上下文,所以任务内部的try-except无法捕获。time.sleep()的正常情况
time.sleep()是Python普通阻塞调用,不会修改进程的信号处理机制。当Celery发送SIGUSR1信号时,默认信号处理器正常触发,SoftTimeLimitExceeded异常能被任务线程的try-except捕获。
内容的提问来源于stack exchange,提问作者bcsta

