前言:问题再现
上次讲了一个关于多线程任务丢失处理的思考,介绍了三个解决方案:多线程加停顿、改用单线程、使用多线程多 Token。【补充链接:业务实战记录4:多维表插入数据任务丢失处理与思考】
这段时间有一些“续集”,来补充一下:
先讲一下结论,就是通过前两种方法,还是有一些其他的问题。
下面挨个展开说说。
上次遇到该问题加了停顿,然后测试两次,都没有问题,所以就得出了这个加停顿有效的结论。事实上,它确实是有效的,但是它仅仅解决了请求太多次导致的任务丢失问题,这并不能解决其他的问题,比如说:Discover a new version, please refresh the page.
,所以还是会有错误。这个通过停顿无法解决,需要思考其他的解决方案。
而改用单线程,这个也能一定程度上解决因为请求太多次导致的任务丢失问题,但是当同步的数据量比较大的时候,会被限制,出现Connection is closed.
报错。
单线程由于请求时间过长,会被关闭连接,这是一个明显的瓶颈,因此我们需要回归多线程的方案,考虑从多线程寻找解决方案。
那么问题来了,当出现报错时,能不能将其捕捉到,并对其进行重跑呢?如果能又该怎么实现?
如果能对报错的任务进行捕捉和重跑,将能提高代码的效率和保证代码的稳定性。事实上是可以的,前人已经将这类问题解决了,并提供了相关的接口给我们去调用。
初步方案:捕获错误任务并重跑
不过代码有点长,具体如下:
第 17~32 行是正常执行任务,第 34~50 行是执行错误的任务。
from concurrent.futures import ThreadPoolExecutor, as_completed
import concurrent.futures
import random
import time
def do_work(task_id):
wait_time = random.randint(1, 3)
time.sleep(wait_time)
if random.random() < 0.25: # 随机抛出异常
raise Exception(f"Task {task_id} failed")
return task_id, wait_time
def main():
task_ids = list(range(10)) # 总共 10 个任务
failed_tasks = [] # 用于记录失败的任务
with ThreadPoolExecutor(max_workers=5) as executor:
future_to_task = {executor.submit(do_work, task_id): task_id for task_id in task_ids}
# 添加 wait 语句,等待任务完成或超时
# 添加了 wait(future_to_task) 语句。这样可以确保在执行 as_completed 之前,wait 函数会等待所有任务完成或超时。
# 若要设置超时,可以在括号里添加 timeout 参数,如:wait(future_to_task, timeout=10)
concurrent.futures.wait(future_to_task)
for future in as_completed(future_to_task):
task_id = future_to_task[future]
try:
task_id, wait_time = future.result()
print(f"Task {task_id} completed, waited for {wait_time} seconds")
except Exception as e:
print(f"Task {task_id} failed: {e}")
failed_tasks.append(task_id)
if failed_tasks:
print("Retrying failed tasks...")
with ThreadPoolExecutor(max_workers=5) as executor:
future_to_task = {executor.submit(do_work, task_id): task_id for task_id in failed_tasks}
# 添加 wait 语句,等待任务完成或超时
# 添加了 wait(future_to_task) 语句。这样可以确保在执行 as_completed 之前,wait 函数会等待所有任务完成或超时。
# 若要设置超时,可以在括号里添加 timeout 参数,如:wait(future_to_task, timeout=10)
concurrent.futures.wait(future_to_task)
for future in as_completed(future_to_task):
task_id = future_to_task[future]
try:
task_id, wait_time = future.result()
print(f"Retried task {task_id} completed, waited for {wait_time} seconds")
except Exception as e:
print(f"Retried task {task_id} still failed: {e}")
if __name__ == "__main__":
main()
以上代码成功的将错误的任务捕捉到了,还进行了重跑。不过由于任务量比较小,只有10个,所以使用以上代码重跑一次,大概率不错出现二次报错,如果你把任务数task_ids
加到三五十个,这个时候就会发现,重跑一次还是会有报错!
最终方案:内函数循环执行错误任务
新问题:怎么不断地进行错误的捕捉和重跑,以保证任务都能够正常执行?
可以通过内函数进行循环调用。
将以上代码处理报错部分(第 34~50 行)提取出来,创建一个新的函数,然后调用自己即可。
参考代码如下:
from concurrent.futures import ThreadPoolExecutor, as_completed
import concurrent.futures
import random
import time
def do_work(task_id):
wait_time = random.randint(1, 3)
time.sleep(wait_time)
if random.random() < 0.25: # 随机抛出异常
raise Exception(f"Task {task_id} failed")
return task_id, wait_time
def main():
task_ids = list(range(40)) # 总共 10 个任务
failed_tasks = [] # 用于记录失败的任务
with ThreadPoolExecutor(max_workers=5) as executor:
future_to_task = {executor.submit(do_work, task_id): task_id for task_id in task_ids}
# 添加 wait 语句,等待任务完成或超时(可以不加)
# 添加了 wait(future_to_task) 语句。这样可以确保在执行 as_completed 之前,wait 函数会等待所有任务完成或超时。
# 若要设置超时,可以在括号里添加 timeout 参数,如:wait(future_to_task, timeout=10)
concurrent.futures.wait(future_to_task)
for future in as_completed(future_to_task):
task_id = future_to_task[future]
try:
task_id, wait_time = future.result()
print(f"Task {task_id} completed, waited for {wait_time} seconds")
except Exception as e:
print(f"Task {task_id} failed: {e}")
failed_tasks.append(task_id)
redo_failed_tasks(failed_tasks)
def redo_failed_tasks(failed_tasks):
"""处理错误任务,内函数反复执行,直到成功!"""
if failed_tasks:
print("Retrying failed tasks...")
with ThreadPoolExecutor(max_workers=5) as executor:
future_to_task = {executor.submit(do_work, task_id): task_id for task_id in failed_tasks}
# 添加 wait 语句,等待任务完成或超时(可以不加)
# 添加了 wait(future_to_task) 语句。这样可以确保在执行 as_completed 之前,wait 函数会等待所有任务完成或超时。
# 若要设置超时,可以在括号里添加 timeout 参数,如:wait(future_to_task, timeout=10)
concurrent.futures.wait(future_to_task)
failed_tasks = [] # 用于记录失败的任务,提交任务之后置空,重新记录失败任务,以便再次提交
for future in as_completed(future_to_task):
task_id = future_to_task[future]
try:
task_id, wait_time = future.result()
print(f"Retried task {task_id} completed, waited for {wait_time} seconds")
except Exception as e:
print(f"Retried task {task_id} still failed: {e}")
failed_tasks.append(task_id)
else:
return
redo_failed_tasks(failed_tasks)
if __name__ == "__main__":
main()
该代码将错误任务重跑代码封装到redo_failed_tasks()
函数,函数中加了处理新失败任务的逻辑,即第 50 和 59 行,并加了内函数调用自己,如果当没有失败任务时则返回,不再自调用。然后再main()
函数中调用该报错函数。
这是抽象出来的逻辑,实际在跑同步的任务,需要给任务加一个编号,类似这里的task_id
,而且每个task_id
对应的数据也需要传递到错误任务的函数(redo_failed_tasks()
)中,以便重新提交。
一个task_id
对应一个数据记录,这很符合字典的逻辑,所以我在处理的过程直接将failed_tasks
更换为字典,将task_id
作为键,需要重新提交的数据作为值传递到redo_failed_tasks()
中,当然还有一些细节,比如说执行任务的函数do_work()
也需要传递对应的数据进行提交。
小结
本文探讨了如何处理在开启concurrent.futures.ThreadPoolExecutor
多线程执行任务时出现的错误任务。介绍了如何对错误任务进行捕捉和重新提交,以确保所有任务成功执行的方法。该方法其实也可以用于解决上一篇文章所遇到的访问太频繁导致任务丢失的问题。
本文提供的代码相对比较通用,没有太多场景的定制,所以在跑实际业务时需要进行定制化,以适配不同的业务场景。
- End -