#-*- coding: utf-8 -*-#@Author : ydf
"""类celery的worker模式,可用于一切需要分布式并发的地方,最好是io类型的。可以分布式调度起一切函数。
rabbitmq生产者和消费者框架。完全实现了celery worker模式的全部功能,使用更简单。支持自动重试指定次数,消费确认,指定数量的并发线程,和指定频率控制1秒钟只运行几次, 同时对mongodb类型的异常做了特殊处理
最开始写得是使用pika包,非线程安全,后来加入rabbitpy,rabbitpy包推送会丢失部分数据,推荐pika包使用
单下划线代表保护,双下划线代表私有。只要关注公有方法就可以,其余是类内部自调用方法。
3月15日
1)、新增RedisConsumer 是基于redis中间件的消费框架,不支持随意暂停程序或者断点,会丢失一部分正在运行中的任务,推荐使用rabbitmq的方式。
get_consumer是使用工厂模式来生成基于rabbit和reids的消费者,使用不同中间件的消费框架更灵活一点点,只需要修改一个数字。
3月20日
2)、增加支持函数参数过滤的功能,可以随时放心多次推送相同的任务到中间件,会先检查该任务是否需要执行,避免浪费cpu和流量,加快处理速度。
基于函数参数值的过滤,需要设置 do_task_filtering 参数为True才生效,默认为False。
3)、新增支持了函数的参数是多个参数,需要设置is_consuming_function_use_multi_params 为True生效,为了兼容老代码默认为False。
区别是消费函数原来需要
def f(body): # 函数有且只能有一个参数,是字典的多个键值对来表示参数的值。
print(body['a'])
print(body['b'])
现在可以
def f(a,b):
print(a)
print(b)
对于推送的部分,都是一样的,都是推送 {"a":1,"b":2}
6月3日
1) 增加了RedisPublisher类,和增加get_publisher工厂模式
方法同mqpublisher一样,这是为了增强一致性,以后每个业务的推送和消费,如果不直接使用RedisPublisher RedisConsumerer RabbitmqPublisher RabbitMQConsumer这些类,而是使用get_publisher和get_consumer来获取发布和消费对象,支持修改一个全局变量的broker_kind数字来切换所有平台消费和推送的中间件种类。
2)增加指定不运行的时间的配置。例如可以白天不运行,只在晚上运行。
3)增加了函数超时的配置,当函数运行时间超过n秒后,自动杀死函数,抛出异常。
4) 增加每分钟函数运行次数统计,和按照最近一分钟运行函数次数来预估多久可以运行完成当前队列剩余的任务。
5) 增加一个判断函数,阻塞判断连续多少分钟队列里面是空的。判断任务疑似完成。
6)增加一个终止消费者的标志,设置标志后终止循环调度消息。
7) consumer对象增加内置一个属性,表示相同队列名的publisher实例。"""
#import functools
importabcimportcopyimporttracebackimporttypingimportjsonfrom collections importCallable, OrderedDictimporttimefrom concurrent.futures importThreadPoolExecutorfrom functools importwrapsfrom threading importLock, Threadimportunittestimportrabbitpyfrom pika importBasicProperties#noinspection PyUnresolvedReferences
from pika.exceptions importChannelClosed, AMQPError#from rabbitpy.message import Properties
importpikafrom pika.adapters.blocking_connection importBlockingChannelfrom pymongo.errors importPyMongoErrorfrom app.utils_ydf import(LogManager, LoggerMixin, RedisMixin, BoundedThreadPoolExecutor, RedisBulkWriteHelper, RedisOperation, decorators, time_util, LoggerLevelSetterMixin, nb_print)from app importconfig as app_config#LogManager('pika').get_logger_and_add_handlers(10)#LogManager('pika.heartbeat').get_logger_and_add_handlers(10)#LogManager('rabbitpy').get_logger_and_add_handlers(10)#LogManager('rabbitpy.base').get_logger_and_add_handlers(10)
defdelete_keys_from_dict(dictx: dict, keys: list):for dict_key inkeys:
dictx.pop(dict_key)classExceptionForRetry(Exception):"""为了重试的,抛出错误。只是定义了一个子类,用不用都可以"""
classExceptionForRequeue(Exception):"""框架检测到此错误,重新放回队列中"""
class ExceptionForRabbitmqRequeue(ExceptionForRequeue): #以后去掉这个异常,抛出上面那个异常就可以了。
"""遇到此错误,重新放回队列中"""
classRabbitmqClientRabbitPy:"""使用rabbitpy包。"""
#noinspection PyUnusedLocal
def __init__(self, username, password, host, port, virtual_host, heartbeat=0):
rabbit_url= f'amqp://{username}:{password}@{host}:{port}/{virtual_host}?heartbeat={heartbeat}'self.connection=rabbitpy.Connection(rabbit_url)def creat_a_channel(self) ->rabbitpy.AMQP:return rabbitpy.AMQP(self.connection.channel()) #使用适配器,使rabbitpy包的公有方法几乎接近pika包的channel的方法。
classRabbitmqClientPika:"""使用pika包,多线程不安全的包。"""
def __init__(self, username, password, host, port, virtual_host, heartbeat=0):"""parameters = pika.URLParameters('amqp://guest:guest@localhost:5672/%2F')
connection = pika.SelectConnection(parameters=parameters,
on_open_callback=on_open)
:param username:
:param password:
:param host:
:param port:
:param virtual_host:
:param heartbeat:"""credentials=pika.PlainCredentials(username, password)
self.connection=pika.BlockingConnection(pika.ConnectionParameters(
host, port, virtual_host, credentials, heartbeat=heartbeat))def creat_a_channel(self) ->BlockingChannel:returnself.connection.channel()classRabbitMqFactory:def __init__(self, username=app_config.RABBITMQ_USER, password=app_config.RABBITMQ_PASS, host=app_config.RABBITMQ_HOST, port=app_config.RABBITMQ_PORT, virtual_host=app_config.RABBITMQ_VIRTUAL_HOST, heartbeat=60 * 10, is_use_rabbitpy=0):""":param username:
:param password:
:param port:
:param virtual_host:
:param heartbeat:
:param is_use_rabbitpy: 为0使用pika,多线程不安全。为1使用rabbitpy,多线程安全的包。"""
ifis_use_rabbitpy:
self.rabbit_client=RabbitmqClientRabbitPy(username, password, host, port, virtual_host, heartbeat)else:
self.rabbit_client=RabbitmqClientPika(username, password, host, port, virtual_host, heartbeat)defget_rabbit_cleint(self):returnself.rabbit_clientclass AbstractPublisher(LoggerLevelSetterMixin, metaclass=abc.ABCMeta, ):def __init__(self, queue_name, log_level_int=10, logger_prefix='', is_add_file_handler=True, clear_queue_within_init=False):""":param queue_name:
:param log_level_int:
:param logger_prefix:
:param is_add_file_handler:
:param clear_queue_within_init:"""self._queue_name=queue_nameif logger_prefix != '':
logger_prefix+= '--'logger_name= f'{logger_prefix}{self.__class__.__name__}--{queue_name}'self.logger= LogManager(logger_name).get_logger_and_add_handlers(log_level_int, log_filename=f'{logger_name}.log' if is_add_file_handler else None) # #self.rabbit_client = RabbitMqFactory(is_use_rabbitpy=is_use_rabbitpy).get_rabbit_cleint()
#self.channel = self.rabbit_client.creat_a_channel()
#self.queue = self.channel.queue_declare(queue=queue_name, durable=True)
self._lock_for_pika =Lock()
self._lock_for_count=Lock()
self._current_time=None
self.count_per_minute=None
self._init_count()
self.init_broker()
self.logger.info(f'{self.__class__} 被实例化了')
self.publish_msg_num_total=0ifclear_queue_within_init:
self.clear()def_init_count(self):
with self._lock_for_count:
self._current_time=time.time()
self.count_per_minute=0
@abc.abstractmethoddefinit_broker(self):pass
defpublish(self, msg: typing.Union[str, dict]):ifisinstance(msg, dict):
msg=json.dumps(msg)
t_start=time.time()
decorators.handle_exception(retry_times=10, is_throw_error=True, time_sleep=0.1)(self.concrete_realization_of_publish)(msg)
self.logger.debug(f'向{self._queue_name} 队列,推送消息 耗时{round(time.time() - t_start, 5)}秒 {msg}')
with self._lock_for_count:
self.count_per_minute+= 1self.publish_msg_num_total+= 1
if time.time() - self._current_time > 10:
self.logger.info(f'10秒内推送了 {self.count_per_minute} 条消息,累计推送了 {self.publish_msg_num_total} 条消息到 {self._queue_name} 中')
self._init_count()
@abc.abstractmethoddefconcrete_realization_of_publish(self, msg):raiseNotImplementedError
@abc.abstractmethoddefclear(self):raiseNotImplementedError
@abc.abstractmethoddefget_message_count(self):raiseNotImplementedError
@abc.abstractmethoddefclose(self):raiseNotImplementedErrordef __enter__(self):returnselfdef __exit__(self, exc_type, exc_val, exc_tb):
self.close()
self.logger.warning(f'with中自动关闭publisher连接,累计推送了 {self.publish_msg_num_total} 条消息')defdeco_mq_conn_error(f):def _inner(self, *args, **kwargs):try:return f(self, *args, **kwargs)exceptAMQPError as e:
self.logger.error(f'rabbitmq链接出错 ,方法 {f.__name__} 出错 ,{e}')
self.init_broker()return f(self, *args, **kwargs)return_innerclassRabbitmqPublisher(AbstractPublisher):"""使用pika实现的。"""
#noinspection PyAttributeOutsideInit
definit_broker(self):
self.logger.warning(f'使用pika 链接mq')
self.rabbit_client= RabbitMqFactory(is_use_rabbitpy=0).get_rabbit_cleint()
self.channel=self.rabbit_client.creat_a_channel()
self.queue= self.channel.queue_declare(queue=self._queue_name, durable=True)#noinspection PyAttributeOutsideInit
@deco_mq_conn_errordefconcrete_realization_of_publish(self, msg):
with self._lock_for_pika:#亲测pika多线程publish会出错。
#if self.channel.connection.is_closed or self.channel.is_closed: # 有时候断了。
#self.logger.critical('发布消息,pika链接断了 “self.channel.connection.is_closed or self.channel.is_closed ”')
#self.rabbit_client = RabbitMqFactory(is_use_rabbitpy=0).get_rabbit_cleint()
#self.channel = self.rabbit_client.creat_a_channel()
#self.queue = self.channel.queue_declare(queue=self._queue_name, durable=True)
#import random
#if random.randint(0, 3) != 1:
#raise AMQPError
self.channel.basic_publish(exchange='',
routing_key=self._queue_name,
body=msg,
properties=BasicProperties(
delivery_mode=2, #make message persistent 2(1是非持久化)
)
)
@deco_mq_conn_errordefclear(self):
self.channel.queue_purge(self._queue_name)
self.logger.warning(f'清除 {self._queue_name} 队列中的消息成功')
@deco_mq_conn_errordefget_message_count(self):
queue= self.channel.queue_declare(queue=self._queue_name, durable=True)returnqueue.method.message_count#@deco_mq_conn_error
defclose(self):
self.channel.close()
self.rabbit_client.connection.close()
self.logger.warning('关闭pika包 链接')classRabbitmqPublisherUsingRabbitpy(AbstractPublisher):"""使用rabbitpy包实现的。"""
#noinspection PyAttributeOutsideInit
definit_broker(self):
self.logger.warning(f'使用rabbitpy包 链接mq')
self.rabbit_client= RabbitMqFactory(is_use_rabbitpy=1).get_rabbit_cleint()
self.channel=self.rabbit_client.creat_a_channel()
self.queue= self.channel.queue_declare(queue=self._queue_name, durable=True)
@deco_mq_conn_errordefconcrete_realization_of_publish(self, msg):#noinspection PyTypeChecker
self.channel.basic_publish(
exchange='',
routing_key=self._queue_name,
body=msg,
properties={'delivery_mode': 2},
)
@deco_mq_conn_errordefclear(self):
self.channel.queue_purge(self._queue_name)
self.logger.warning(f'清除 {self._queue_name} 队列中的消息成功')
@deco_mq_conn_errordefget_message_count(self):#noinspection PyUnresolvedReferences
ch_raw_rabbity =self.channel.channelreturn rabbitpy.amqp_queue.Queue(ch_raw_rabbity, self._queue_name, durable=True)#@deco_mq_conn_error
defclose(self):
self.channel.close()
self.rabbit_client.connection.close()
self.logger.warning('关闭rabbitpy包 链接mq')classRedisPublisher(AbstractPublisher, RedisMixin):"""使用redis作为中间件"""
definit_broker(self):pass
defconcrete_realization_of_publish(self, msg):#noinspection PyTypeChecker
self.redis_db7.rpush(self._queue_name, msg)defclear(self):
self.redis_db7.delete(self._queue_name)
self.logger.warning(f'清除 {self._queue_name} 队列中的消息成功')defget_message_count(self):returnself.redis_db7.llen(self._queue_name)defclose(self):#self.redis_db7.connection_pool.disconnect()
pass
classRedisFilter(RedisMixin):def __init__(self, redis_key_name):
self._redis_key_name=redis_key_name
@staticmethoddef_get_ordered_str(value):"""对json的键值对在redis中进行过滤,需要先把键值对排序,否则过滤会不准确如 {"a":1,"b":2} 和 {"b":2,"a":1}"""
ifisinstance(value, str):
value=json.loads(value)
ordered_dict=OrderedDict()for k insorted(value):
ordered_dict[k]=value[k]returnjson.dumps(ordered_dict)defadd_a_value(self, value: typing.Union[str, dict]):
self.redis_db7.sadd(self._redis_key_name, self._get_ordered_str(value))defcheck_value_exists(self, value):returnself.redis_db7.sismember(self._redis_key_name, self._get_ordered_str(value))class AbstractConsumer(LoggerLevelSetterMixin, metaclass=abc.ABCMeta, ):
shedual_task_thread_for_join_on_linux_multiprocessing=list()
time_interval_for_check_do_not_run_time= 60BROKER_KIND=None
@property
@decorators.synchronizeddefpublisher_of_same_queue(self):if notself._publisher_of_same_queue:
self._publisher_of_same_queue= get_publisher(self._queue_name, broker_kind=self.BROKER_KIND)returnself._publisher_of_same_queue
@classmethoddefjoin_shedual_task_thread(cls):""":return:"""
"""def ff():
RabbitmqConsumer('queue_test', consuming_function=f3, threads_num=20, msg_schedule_time_intercal=2, log_level=10, logger_prefix='yy平台消费', is_consuming_function_use_multi_params=True).start_consuming_message()
RabbitmqConsumer('queue_test2', consuming_function=f4, threads_num=20, msg_schedule_time_intercal=4, log_level=10, logger_prefix='zz平台消费', is_consuming_function_use_multi_params=True).start_consuming_message()
AbstractConsumer.join_shedual_task_thread() # 如果开多进程启动消费者,在linux上需要这样写下这一行。
if __name__ == '__main__':
[Process(target=ff).start() for _ in range(4)]"""
for t incls.shedual_task_thread_for_join_on_linux_multiprocessing:
t.join()def __init__(self, queue_name, *, consuming_function: Callable = None, function_timeout=0, threads_num=50, specify_threadpool: ThreadPoolExecutor =None,
max_retry_times=3, log_level=10, is_print_detail_exception=True, msg_schedule_time_intercal=0.0,
logger_prefix='', create_logger_file=True, do_task_filtering=False, is_consuming_function_use_multi_params=True,
is_do_not_run_by_specify_time_effect=False, do_not_run_by_specify_time=('10:00:00', '22:00:00'), schedule_tasks_on_main_thread=False):""":param queue_name:
:param consuming_function: 处理消息的函数。
:param function_timeout : 超时秒数,函数运行超过这个时间,则自动杀死函数。为0是不限制。
:param threads_num:
:param specify_threadpool:使用指定的线程池,可以多个消费者共使用一个线程池,不为None时候。threads_num失效
:param max_retry_times:
:param log_level:
:param is_print_detail_exception:
:param msg_schedule_time_intercal:消息调度的时间间隔,用于控频
:param logger_prefix: 日志前缀,可使不同的消费者生成不同的日志
:param create_logger_file : 是否创建文件日志
:param do_task_filtering :是否执行基于函数参数的任务过滤
:is_consuming_function_use_multi_params 函数的参数是否是传统的多参数,不为单个body字典表示多个参数。
:param is_do_not_run_by_specify_time_effect :是否使不运行的时间段生效
:param do_not_run_by_specify_time :不运行的时间段
:param schedule_tasks_on_main_thread :直接在主线程调度任务,意味着不能直接在当前主线程同时开启两个消费者。"""self._queue_name=queue_name
self.consuming_function=consuming_function
self._function_timeout=function_timeout
self._threads_num=threads_num
self.threadpool= specify_threadpool if specify_threadpool else BoundedThreadPoolExecutor(threads_num + 1) #单独加一个检测消息数量和心跳的线程
self._max_retry_times =max_retry_times
self._is_print_detail_exception=is_print_detail_exception
self._msg_schedule_time_intercal=msg_schedule_time_intercal
self._logger_prefix=logger_prefix
self._log_level=log_levelif logger_prefix != '':
logger_prefix+= '--'logger_name= f'{logger_prefix}{self.__class__.__name__}--{queue_name}'self.logger= LogManager(logger_name).get_logger_and_add_handlers(log_level, log_filename=f'{logger_name}.log' if create_logger_file elseNone)
self.logger.info(f'{self.__class__} 被实例化')
self._do_task_filtering=do_task_filtering
self._redis_filter_key_name= f'filter:{queue_name}'self._redis_filter=RedisFilter(self._redis_filter_key_name)
self._is_consuming_function_use_multi_params=is_consuming_function_use_multi_params
self._lock_for_pika=Lock()
self._execute_task_times_every_minute= 0 #每分钟执行了多少次任务。
self._lock_for_count_execute_task_times_every_minute =Lock()
self._current_time_for_execute_task_times_every_minute=time.time()
self._msg_num_in_broker=0
self._last_timestamp_when_has_task_in_queue=0
self._last_timestamp_print_msg_num=0
self._is_do_not_run_by_specify_time_effect=is_do_not_run_by_specify_time_effect
self._do_not_run_by_specify_time= do_not_run_by_specify_time #可以设置在指定的时间段不运行。
self._schedule_tasks_on_main_thread =schedule_tasks_on_main_thread
self.stop_flag=False
self._publisher_of_same_queue=Nonedef keep_circulating(self, time_sleep=0.001, exit_if_function_run_sucsess=False, is_display_detail_exception=True):"""间隔一段时间,一直循环运行某个方法的装饰器
:param time_sleep :循环的间隔时间
:param is_display_detail_exception
:param exit_if_function_run_sucsess :如果成功了就退出循环"""
def_keep_circulating(func):#noinspection PyBroadException
@wraps(func)def __keep_circulating(*args, **kwargs):while 1:ifself.stop_flag:break
try:
result= func(*args, **kwargs)ifexit_if_function_run_sucsess:returnresultexceptException as e:
msg= func.__name__ + '运行出错\n' + traceback.format_exc(limit=10) if is_display_detail_exception elsestr(e)
self.logger.error(msg)finally:
time.sleep(time_sleep)return __keep_circulating
return_keep_circulatingdefstart_consuming_message(self):#self.threadpool.submit(decorators.keep_circulating(20)(self.check_heartbeat_and_message_count))
self.threadpool.submit(self.keep_circulating(20)(self.check_heartbeat_and_message_count))ifself._schedule_tasks_on_main_thread:#decorators.keep_circulating(1)(self._shedual_task)()
self.keep_circulating(1)(self._shedual_task)()else:#t = Thread(target=decorators.keep_circulating(1)(self._shedual_task))
t = Thread(target=self.keep_circulating(1)(self._shedual_task))
self.__class__.shedual_task_thread_for_join_on_linux_multiprocessing.append(t)
t.start()
@abc.abstractmethoddef_shedual_task(self):raiseNotImplementedErrordef _run_consuming_function_with_confirm_and_retry(self, kw: dict, current_retry_times=0):if self._do_task_filtering and self._redis_filter.check_value_exists(kw['body']): #对函数的参数进行检查,过滤已经执行过并且成功的任务。
self.logger.info(f'redis的 [{self._redis_filter_key_name}] 键 中 过滤任务 {kw["body"]}')
self._confirm_consume(kw)returnwith self._lock_for_count_execute_task_times_every_minute:
self._execute_task_times_every_minute+= 1
if time.time() - self._current_time_for_execute_task_times_every_minute > 60:
self.logger.info(
f'一分钟内执行了 {self._execute_task_times_every_minute} 次函数 [ {self.consuming_function.__name__} ] ,预计'f'还需要 {time_util.seconds_to_hour_minute_second(self._msg_num_in_broker / self._execute_task_times_every_minute * 60)} 时间'f'才能执行完成 {self._msg_num_in_broker}个剩余的任务')
self._current_time_for_execute_task_times_every_minute=time.time()
self._execute_task_times_every_minute=0if current_retry_times < self._max_retry_times + 1:#noinspection PyBroadException
t_start =time.time()try:
function_run= self.consuming_function if self._function_timeout == 0 elsedecorators.timeout(self._function_timeout)(self.consuming_function)if self._is_consuming_function_use_multi_params: #消费函数使用传统的多参数形式
function_run(**kw['body'])else:
function_run(kw['body']) #消费函数使用单个参数,参数自身是一个字典,由键值对表示各个参数。
self._confirm_consume(kw)ifself._do_task_filtering:
self._redis_filter.add_a_value(kw['body']) #函数执行成功后,添加函数的参数排序后的键值对字符串到set中。
self.logger.debug(f'函数 {self.consuming_function.__name__} 第{current_retry_times + 1}次 运行, 正确了,函数运行时间是 {round(time.time() - t_start, 2)} 秒,入参是 【 {kw["body"]} 】')exceptException as e:if isinstance(e, (PyMongoError, ExceptionForRequeue)): #mongo经常维护备份时候插入不了或挂了,或者自己主动抛出一个ExceptionForRequeue类型的错误会重新入队,不受指定重试次数逇约束。
self.logger.critical(f'函数 [{self.consuming_function.__name__}] 中发生错误 {type(e)} {e}')returnself._requeue(kw)
self.logger.error(f'函数 {self.consuming_function.__name__} 第{current_retry_times + 1}次发生错误,函数运行时间是 {round(time.time() - t_start, 2)} 秒,\n 入参是 【 {kw["body"]} 】 \n 原因是 {type(e)}', exc_info=self._is_print_detail_exception)
self._run_consuming_function_with_confirm_and_retry(kw, current_retry_times+ 1)else:
self.logger.critical(f'函数 {self.consuming_function.__name__} 达到最大重试次数 {self._max_retry_times} 后,仍然失败, 入参是 【 {kw["body"]} 】') #错得超过指定的次数了,就确认消费了。
self._confirm_consume(kw)
@abc.abstractmethoddef_confirm_consume(self, kw):"""确认消费"""
raiseNotImplementedError#noinspection PyUnusedLocal
defcheck_heartbeat_and_message_count(self):
self._msg_num_in_broker=self.publisher_of_same_queue.get_message_count()if time.time() - self._last_timestamp_print_msg_num > 60:
self.logger.info(f'[{self._queue_name}] 队列中还有 [{self._msg_num_in_broker}] 个任务')
self._last_timestamp_print_msg_num=time.time()if self._msg_num_in_broker !=0:
self._last_timestamp_when_has_task_in_queue=time.time()returnself._msg_num_in_broker
@abc.abstractmethoddef_requeue(self, kw):"""重新入队"""
raiseNotImplementedErrordef_submit_task(self, kw):ifself._judge_is_daylight():
self._requeue(kw)
time.sleep(self.time_interval_for_check_do_not_run_time)returnself.threadpool.submit(self._run_consuming_function_with_confirm_and_retry, kw)def_judge_is_daylight(self):if self._is_do_not_run_by_specify_time_effect and self._do_not_run_by_specify_time[0] < time_util.DatetimeConverter().time_str < self._do_not_run_by_specify_time[1]:
self.logger.warning(f'现在时间是 {time_util.DatetimeConverter()} ,现在时间是在 {self._do_not_run_by_specify_time} 之间,不运行')returnTruedef wait_for_possible_has_finish_all_tasks(self, minutes: int, mannu_call_check_heartbeat_and_message_count=False, stop_flag=0):"""由于是异步消费,和存在队列一边被消费,一边在推送,或者还有结尾少量任务还在确认消费者实际还没彻底运行完成。 但有时候需要判断 所有任务,务是否完成,提供一个不精确的判断,要搞清楚原因和场景后再慎用。
:param minutes 连续多少分钟没任务就判断为消费已完成
:param mannu_call_check_heartbeat_and_message_count 如果消费者没有执行startconsuming,需要手动调用这个方法
:param stop_flag 设置停止标志。停止当前实例无限循环调度消息。
:return:"""
if minutes <= 1:raise ValueError('疑似完成任务,判断时间最少需要设置为2分钟内,每隔20秒检测一次都是0个任务,')ifmannu_call_check_heartbeat_and_message_count:
self.threadpool= BoundedThreadPoolExecutor(2)
self.threadpool.submit(self.keep_circulating(20)(self.check_heartbeat_and_message_count))whileTrue:if minutes * 60 < time.time() - self._last_timestamp_when_has_task_in_queue < 3650 * 24 * 60 * 60: #初次时间戳是0,确保不是无限大。
#print(self._last_timestamp_print_msg_num)
self.logger.warning(f'最后一次有任务的时间是{time_util.DatetimeConverter(self._last_timestamp_when_has_task_in_queue)},已经有 {minutes} 分钟没有任务了,疑似完成。')
self.stop_flag=stop_flagifself.stop_flag:
self.logger.warning('当前实例退出循环调度消息')break
else:
time.sleep(30)"""continuou_no_task_times = 0
check_interval_time = 10
while True:
try:
msg_num_in_broker = self.check_heartbeat_and_message_count()
except Exception:
msg_num_in_broker = 9999
if msg_num_in_broker == 0:
continuou_no_task_times += 1
else:
continuou_no_task_times = 0
if continuou_no_task_times >= minutes * (60//check_interval_time):
break
time.sleep(check_interval_time)"""
classRabbitmqConsumer(AbstractConsumer):"""使用pika包实现的。"""BROKER_KIND=0def_shedual_task_old(self):
channel= RabbitMqFactory(is_use_rabbitpy=0).get_rabbit_cleint().creat_a_channel()
channel.queue_declare(queue=self._queue_name, durable=True)
channel.basic_qos(prefetch_count=self._threads_num)defcallback(ch, method, properties, body):
body=body.decode()
self.logger.debug(f'从rabbitmq的 [{self._queue_name}] 队列中 取出的消息是: {body}')
time.sleep(self._msg_schedule_time_intercal)
body=json.loads(body)
kw= {'ch': ch, 'method': method, 'properties': properties, 'body': body}
self._submit_task(kw)ifself.stop_flag:
ch.close()#使start_consuming结束。
channel.basic_consume(callback,
queue=self._queue_name,#no_ack=True
)
channel.start_consuming()def_shedual_task(self):
channel= RabbitMqFactory(is_use_rabbitpy=0).get_rabbit_cleint().creat_a_channel()
channel.queue_declare(queue=self._queue_name, durable=True)
channel.basic_qos(prefetch_count=self._threads_num)whileTrue:ifself.stop_flag:returnmethod, properties, body= channel.basic_get(self._queue_name, no_ack=False)if body isNone:
time.sleep(0.001)else:
body=body.decode()
self.logger.debug(f'从rabbitmq的 [{self._queue_name}] 队列中 取出的消息是: {body}')
body=json.loads(body)
kw= {'ch': channel, 'method': method, 'properties': properties, 'body': body}
self._submit_task(kw)
time.sleep(self._msg_schedule_time_intercal)def_confirm_consume(self, kw):
with self._lock_for_pika:
kw['ch'].basic_ack(delivery_tag=kw['method'].delivery_tag) #确认消费
def_requeue(self, kw):
with self._lock_for_pika:#ch.connection.add_callback_threadsafe(functools.partial(self.__ack_message_pika, ch, method.delivery_tag))
return kw['ch'].basic_nack(delivery_tag=kw['method'].delivery_tag) #立即重新入队。
@staticmethoddef __ack_message_pika(channelx, delivery_tagx):"""Note that `channel` must be the same pika channel instance via which
the message being ACKed was retrieved (AMQP protocol constraint)."""
ifchannelx.is_open:
channelx.basic_ack(delivery_tagx)else:#Channel is already closed, so we can't ACK this message;
#log and/or do something that makes sense for your app in this case.
pass
classRabbitmqConsumerRabbitpy(AbstractConsumer):"""使用rabbitpy实现的"""BROKER_KIND= 1
def_shedual_task(self):#noinspection PyTypeChecker
channel = RabbitMqFactory(is_use_rabbitpy=1).get_rabbit_cleint().creat_a_channel() #type: rabbitpy.AMQP #
channel.queue_declare(queue=self._queue_name, durable=True)
channel.basic_qos(prefetch_count=self._threads_num)for message in channel.basic_consume(self._queue_name, no_ack=False):
body=message.body.decode()
self.logger.debug(f'从rabbitmq {self._queue_name} 队列中 取出的消息是: {body}')
time.sleep(self._msg_schedule_time_intercal)
kw= {'message': message, 'body': json.loads(message.body.decode())}ifself.stop_flag:return
#channel.channel.close()
self._submit_task(kw)def_confirm_consume(self, kw):
kw['message'].ack()def_requeue(self, kw):
kw['message'].nack(requeue=True)classRedisConsumer(AbstractConsumer, RedisMixin):"""redis作为中间件实现的。"""BROKER_KIND= 2
def_shedual_task_old(self):whileTrue:
t_start=time.time()
task_bytes= self.redis_db7.blpop(self._queue_name)[1] #使用db7
iftask_bytes:
task_dict=json.loads(task_bytes)#noinspection PyProtectedMember
self.logger.debug(f'取出的任务时间是 {round(time.time() - t_start, 2)} 消息是: {task_bytes.decode()}')
time.sleep(self._msg_schedule_time_intercal)
kw= {'body': task_dict}ifself.stop_flag:returnself._submit_task(kw)def _shedual_task(self): #这样容易控制退出消费循环。
whileTrue:ifself.stop_flag:returnt_start=time.time()
task_bytes= self.redis_db7.lpop(self._queue_name) #使用db7
iftask_bytes:
task_dict=json.loads(task_bytes)#noinspection PyProtectedMember
self.logger.debug(f'取出的任务时间是 {round(time.time() - t_start, 2)} 消息是: {task_bytes.decode()}')
kw= {'body': task_dict}
self._submit_task(kw)else:
time.sleep(0.001)
time.sleep(self._msg_schedule_time_intercal)def_confirm_consume(self, kw):pass #redis没有确认消费的功能。
def_requeue(self, kw):
self.redis_db7.rpush(self._queue_name, json.dumps(kw['body']))def get_publisher(queue_name, *, log_level_int=10, logger_prefix='', is_add_file_handler=False, clear_queue_within_init=False, broker_kind=0):""":param queue_name:
:param log_level_int:
:param logger_prefix:
:param is_add_file_handler:
:param clear_queue_within_init:
:param broker_kind: 中间件或使用包的种类。
:return:"""all_kwargs=copy.deepcopy(locals())
all_kwargs.pop('broker_kind')if broker_kind ==0:return RabbitmqPublisher(**all_kwargs)elif broker_kind == 1:return RabbitmqPublisherUsingRabbitpy(**all_kwargs)elif broker_kind == 2:return RedisPublisher(**all_kwargs)else:raise ValueError('设置的中间件种类数字不正确')def get_consumer(queue_name, *, consuming_function: Callable = None, function_timeout=0, threads_num=50, specify_threadpool: ThreadPoolExecutor =None,
max_retry_times=3, log_level=10, is_print_detail_exception=True, msg_schedule_time_intercal=0.0,
logger_prefix='', create_logger_file=True, do_task_filtering=False, is_consuming_function_use_multi_params=True,
is_do_not_run_by_specify_time_effect=False, do_not_run_by_specify_time=('10:00:00', '22:00:00'), schedule_tasks_on_main_thread=False, broker_kind=0):"""使用工厂模式再包一层,通过设置数字来生成基于不同中间件或包的consumer。
:param queue_name:
:param consuming_function: 处理消息的函数。
:param function_timeout : 超时秒数,函数运行超过这个时间,则自动杀死函数。为0是不限制。
:param threads_num:
:param specify_threadpool:使用指定的线程池,可以多个消费者共使用一个线程池,不为None时候。threads_num失效
:param max_retry_times:
:param log_level:
:param is_print_detail_exception:
:param msg_schedule_time_intercal:消息调度的时间间隔,用于控频
:param logger_prefix: 日志前缀,可使不同的消费者生成不同的日志
:param create_logger_file : 是否创建文件日志
:param do_task_filtering :是否执行基于函数参数的任务过滤
:param is_consuming_function_use_multi_params 函数的参数是否是传统的多参数,不为单个body字典表示多个参数。
:param is_do_not_run_by_specify_time_effect :是否使不运行的时间段生效
:param do_not_run_by_specify_time :不运行的时间段
:param schedule_tasks_on_main_thread :直接在主线程调度任务,意味着不能直接在当前主线程同时开启两个消费者。
:param broker_kind:中间件种类
:return"""all_kwargs=copy.copy(locals())
all_kwargs.pop('broker_kind')if broker_kind ==0:return RabbitmqConsumer(**all_kwargs)elif broker_kind == 1:return RabbitmqConsumerRabbitpy(**all_kwargs)elif broker_kind == 2:return RedisConsumer(**all_kwargs)else:raise ValueError('设置的中间件种类数字不正确')#noinspection PyMethodMayBeStatic,PyShadowingNames
class_Test(unittest.TestCase, LoggerMixin, RedisMixin):"""演示一个简单求和的例子。"""@unittest.skipdeftest_publisher_with(self):"""测试上下文管理器。
:return:"""with RabbitmqPublisher('queue_test') as rp:for i in range(1000):
rp.publish(str(i))
@unittest.skipdeftest_publish_rabbit(self):"""测试mq推送
:return:"""rabbitmq_publisher= RabbitmqPublisher('queue_test', log_level_int=10, logger_prefix='yy平台推送')
rabbitmq_publisher.clear()for i in range(500000):try:
time.sleep(1)
rabbitmq_publisher.publish({'a': i, 'b': 2 *i})exceptException as e:print(e)
rabbitmq_publisher= RabbitmqPublisher('queue_test2', log_level_int=20, logger_prefix='zz平台推送')
rabbitmq_publisher.clear()
[rabbitmq_publisher.publish({'somestr_to_be_print': str(i)}) for i in range(500000)]
@unittest.skipdeftest_publish_redis(self):#如果需要批量推送
for i in range(10007):#最犀利的批量操作方式,自动聚合多条redis命令,支持多种redis混合命令批量操作。
RedisBulkWriteHelper(self.redis_db7, 1000).add_task(RedisOperation('lpush', 'queue_test', json.dumps({'a': i, 'b': 2 *i})))
[self.redis_db7.lpush('queue_test', json.dumps({'a': j, 'b': 2 * j})) for j in range(500)]print('推送完毕')
@unittest.skipdeftest_consume(self):"""单参数代表所有传参
:return:"""
deff(body):
self.logger.info(f'消费此消息 {body}')#print(body['a'] + body['b'])
time.sleep(5) #模拟做某事需要阻塞10秒种,必须用并发。
#把消费的函数名传给consuming_function,就这么简单。
rabbitmq_consumer = RabbitmqConsumer('queue_test', consuming_function=f, threads_num=20, msg_schedule_time_intercal=0.5, log_level=10, logger_prefix='yy平台消费', is_consuming_function_use_multi_params=False)
rabbitmq_consumer.start_consuming_message()
@unittest.skipdeftest_consume2(self):"""测试支持传统参数形式,不是用一个字典里面包含所有参数。
:return:"""
deff2(a, b):
self.logger.debug(f'a的值是 {a}')
self.logger.debug(f'b的值是 {b}')print(f'{a} + {b} 的和是 {a + b}')
time.sleep(3) #模拟做某事需要阻塞10秒种,必须用并发。
#把消费的函数名传给consuming_function,就这么简单。
RabbitmqConsumer('queue_test', consuming_function=f2, threads_num=60, msg_schedule_time_intercal=5, log_level=10, logger_prefix='yy平台消费', is_consuming_function_use_multi_params=True).start_consuming_message()
@unittest.skipdeftest_redis_filter(self):"""测试基于redis set结构的过滤器。
:return:"""redis_filter= RedisFilter('abcd')
redis_filter.add_a_value({'a': 1, 'c': 3, 'b': 2})
redis_filter.check_value_exists({'a': 1, 'c': 3, 'b': 2})
redis_filter.check_value_exists({'a': 1, 'b': 2, 'c': 3})
with decorators.TimerContextManager():print(redis_filter.check_value_exists('{"a": 1, "b": 2, "c": 3}'))
with decorators.TimerContextManager():#实测百万元素的set,过滤检查不需要1毫秒,一般最多100万个酒店。
print(RedisFilter('filter:mafengwo-detail_task').check_value_exists({"_id": "69873340"}))
@unittest.skipdeftest_run_two_function(self):#演示连续运行两个consumer
deff3(a, b):print(f'{a} + {b} = {a + b}')
time.sleep(10) #模拟做某事需要阻塞10秒种,必须用并发。
deff4(somestr_to_be_print):print(f'打印 {somestr_to_be_print}')
time.sleep(20) #模拟做某事需要阻塞10秒种,必须用并发。
RabbitmqConsumer('queue_test', consuming_function=f3, threads_num=20, msg_schedule_time_intercal=2, log_level=10, logger_prefix='yy平台消费', is_consuming_function_use_multi_params=True).start_consuming_message()
RabbitmqConsumer('queue_test2', consuming_function=f4, threads_num=20, msg_schedule_time_intercal=4, log_level=10, logger_prefix='zz平台消费', is_consuming_function_use_multi_params=True).start_consuming_message()#AbstractConsumer.join_shedual_task_thread()
#@unittest.skip
deftest_factory_pattern_consumer(self):"""测试工厂模式来生成消费者
:return:"""
deff2(a, b):#body_dict = json.loads(body)
self.logger.info(f'消费此消息 {a} {b} ,结果是 {a+b}')#print(body_dict['a'] + body_dict['b'])
time.sleep(2) #模拟做某事需要阻塞10秒种,必须用并发。
#把消费的函数名传给consuming_function,就这么简单。
consumer= get_consumer('queue_test5', consuming_function=f2, threads_num=30, msg_schedule_time_intercal=1, log_level=10, logger_prefix='zz平台消费',
function_timeout=20, is_print_detail_exception=True, broker_kind=0) #通过设置broker_kind,一键切换中间件为mq或redis
consumer.publisher_of_same_queue.clear()
[consumer.publisher_of_same_queue.publish({'a': i, 'b': 2 * i}) for i in range(80)]
consumer.start_consuming_message()#consumer.stop_flag = 1
#原则是不需要关闭消费,一直在后台等待任务,循环调度消息。如果需要关闭可以使用下面。
nb_print('判断完成阻塞中。。。')
consumer.wait_for_possible_has_finish_all_tasks(2, stop_flag=1)
nb_print('这一行要等疑似结束判断,才能运行。。。')if __name__ == '__main__':#noinspection PyArgumentList
unittest.main(sleep_time=1)