def use_config_form_distributed_frame_config_module(): """ 自动读取配置。会优先读取启动脚本的目录的distributed_frame_config.py文件。没有则读取项目根目录下的distributed_frame_config.py :return: """ current_script_path = sys.path[0].replace('\\', '/') project_root_path = sys.path[1].replace('\\', '/') inspect_msg = f""" 分布式函数调度框架会自动导入distributed_frame_config模块 当第一次运行脚本时候,函数调度框架会在你的python当前项目的根目录下 {project_root_path} 下,创建一个名为 distributed_frame_config.py 的文件。 自动读取配置,会优先读取启动脚本的所在目录 {current_script_path} 的distributed_frame_config.py文件, 如果没有 {current_script_path}/distributed_frame_config.py 文件,则读取项目根目录 {project_root_path} 下的distributed_frame_config.py做配置。 在 "{project_root_path}/distributed_frame_config.py:1" 文件中,需要按需重新设置要使用到的中间件的键和值,例如没有使用rabbitmq而是使用redis做中间件,则不需要配置rabbitmq。 """ # sys.stdout.write(f'\033[0;33m{time.strftime("%H:%M:%S")}\033[0m "{__file__}:{sys._getframe().f_lineno}" \033[0;30;43m{inspect_msg}\033[0m\n') # noinspection PyProtectedMember if is_main_process(): stdout_write( f'\033[0;93m{time.strftime("%H:%M:%S")}\033[0m "{__file__}:{sys._getframe().f_lineno}" \033[0;93;100m{inspect_msg}\033[0m\n') try: # noinspection PyUnresolvedReferences # import distributed_frame_config m = importlib.import_module('distributed_frame_config') # nb_print(m.__dict__) only_print_on_main_process(f'分布式函数调度框架 读取到\n "{m.__file__}:1" 文件里面的变量作为配置了\n') for var_namex, var_valuex in m.__dict__.items(): if var_namex.isupper(): setattr(frame_config, var_namex, var_valuex) # 用用户自定义的配置覆盖框架的默认配置。 except ModuleNotFoundError: nb_print( f'''分布式函数调度框架检测到 你的项目根目录 {project_root_path} 和当前文件夹 {current_script_path} 下没有 distributed_frame_config.py 文件,\n\n''') auto_creat_config_file_to_project_root_path() show_frame_config()
def __init__(self, queue_name, *, consuming_function: Callable = None, function_timeout=0, concurrent_num=50, specify_concurrent_pool=None, specify_async_loop=None, concurrent_mode=1, max_retry_times=3, log_level=10, is_print_detail_exception=True, msg_schedule_time_intercal=0.0, qps: float = 0, is_using_distributed_frequency_control=False, msg_expire_senconds=0, is_send_consumer_hearbeat_to_redis=False, logger_prefix='', create_logger_file=True, do_task_filtering=False, task_filtering_expire_seconds=0, 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, function_result_status_persistance_conf=FunctionResultStatusPersistanceConfig( False, False, 7 * 24 * 3600), is_using_rpc_mode=False): """ :param queue_name: :param consuming_function: 处理消息的函数。 :param function_timeout : 超时秒数,函数运行超过这个时间,则自动杀死函数。为0是不限制。 # 如果设置了qps,并且cocurrent_num是默认的50,会自动开了500并发,由于是采用的智能线程池任务少时候不会真开那么多线程而且会自动缩小线程数量。具体看ThreadPoolExecutorShrinkAble的说明 # 由于有很好用的qps控制运行频率和智能扩大缩小的线程池,此框架建议不需要理会和设置并发数量只需要关心qps就行了,框架的并发是自适应并发数量,这一点很强很好用。 :param concurrent_num:并发数量,并发种类由concurrent_mode决定 :param specify_concurrent_pool:使用指定的线程池/携程池,可以多个消费者共使用一个线程池,不为None时候。threads_num失效 :param specify_async_loop:指定的async的loop循环,设置并发模式为async才能起作用。 :param concurrent_mode:并发模式,1线程 2gevent 3eventlet 4 asyncio :param max_retry_times: :param log_level: # 这里是设置消费者 发布者日志级别的,如果不想看到很多的细节显示信息,可以设置为 20 (logging.INFO)。 :param is_print_detail_exception: :param msg_schedule_time_intercal:消息调度的时间间隔,用于控频 :param qps:指定1秒内的函数执行次数,qps会覆盖msg_schedule_time_intercal,一会废弃msg_schedule_time_intercal这个参数。 :param is_using_distributed_frequency_control: 是否使用分布式空频(依赖redis计数),默认只对当前实例化的消费者空频有效。假如实例化了2个qps为10的使用同一队列名的消费者, 并且都启动,则每秒运行次数会达到20。如果使用分布式空频则所有消费者加起来的总运行次数是10。 :param is_send_consumer_hearbeat_to_redis 时候将发布者的心跳发送到redis,有些功能的实现需要统计活跃消费者。因为有的中间件不是真mq。 :param logger_prefix: 日志前缀,可使不同的消费者生成不同的日志 :param create_logger_file : 是否创建文件日志 :param do_task_filtering :是否执行基于函数参数的任务过滤 :param task_filtering_expire_seconds:任务过滤的失效期,为0则永久性过滤任务。例如设置过滤过期时间是1800秒 , 30分钟前发布过1 + 2 的任务,现在仍然执行, 如果是30分钟以内发布过这个任务,则不执行1 + 2,现在把这个逻辑集成到框架,一般用于接口价格缓存。 :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 function_result_status_persistance_conf :配置。是否保存函数的入参,运行结果和运行状态到mongodb。 这一步用于后续的参数追溯,任务统计和web展示,需要安装mongo。 :param is_using_rpc_mode 是否使用rpc模式,可以在发布端获取消费端的结果回调,但消耗一定性能,使用async_result.result时候会等待阻塞住当前线程。 执行流程为 1、 实例化消费者类,设置各种控制属性 2、启动 start_consuming_message 启动消费 3、start_consuming_message 中 调用 _shedual_task 从中间件循环取消息 4、 _shedual_task 中调用 _submit_task,将 任务 添加到并发池中并发运行。 5、 函数执行完成后,运行 _confirm_consume , 确认消费。 各种中间件的 取消息、确认消费 单独实现,其他逻辑由于采用了模板模式,自动复用代码。 """ self.init_params = copy.copy(locals()) self.init_params.pop('self') self.init_params['broker_kind'] = self.__class__.BROKER_KIND self.init_params['consuming_function'] = consuming_function ConsumersManager.consumers_queue__info_map[queue_name] = current_queue__info_dict = copy.copy(self.init_params) current_queue__info_dict['consuming_function'] = str(consuming_function) # consuming_function.__name__ current_queue__info_dict['specify_async_loop'] = str(specify_async_loop) current_queue__info_dict[ 'function_result_status_persistance_conf'] = function_result_status_persistance_conf.to_dict() current_queue__info_dict['class_name'] = self.__class__.__name__ concurrent_name = ConsumersManager.get_concurrent_name_by_concurrent_mode(concurrent_mode) current_queue__info_dict['concurrent_mode_name'] = concurrent_name # 方便点击跳转定位到当前解释器下所有实例化消费者的文件行,点击可跳转到该处。 # 获取被调用函数在被调用时所处代码行数 # 直接实例化相应的类和使用工厂模式来实例化相应的类,得到的消费者实际实例化的行是不一样的,希望定位到用户的代码处,而不是定位到工厂模式处。也不要是task_deco装饰器本身处。 line = sys._getframe(0).f_back.f_lineno # 获取被调用函数所在模块文件名 file_name = sys._getframe(1).f_code.co_filename if 'consumer_factory.py' in file_name: line = sys._getframe(1).f_back.f_lineno file_name = sys._getframe(2).f_code.co_filename if r'function_scheduling_distributed_framework\__init__.py' in file_name or 'function_scheduling_distributed_framework/__init__.py' in file_name: line = sys._getframe(2).f_back.f_lineno file_name = sys._getframe(3).f_code.co_filename current_queue__info_dict['where_to_instantiate'] = f'{file_name}:{line}' self._queue_name = queue_name self.queue_name = queue_name # 可以换成公有的,免得外部访问有警告。 if consuming_function is None: raise ValueError('必须传 consuming_function 参数') self.consuming_function = consuming_function self._function_timeout = function_timeout # 如果设置了qps,并且cocurrent_num是默认的50,会自动开了500并发,由于是采用的智能线程池任务少时候不会真开那么多线程而且会自动缩小线程数量。具体看ThreadPoolExecutorShrinkAble的说明 # 由于有很好用的qps控制运行频率和智能扩大缩小的线程池,此框架建议不需要理会和设置并发数量只需要关心qps就行了,框架的并发是自适应并发数量,这一点很强很好用。 if qps != 0 and concurrent_num == 50: self._concurrent_num = 500 else: self._concurrent_num = concurrent_num self._specify_concurrent_pool = specify_concurrent_pool self._specify_async_loop = specify_async_loop self._concurrent_pool = None self._concurrent_mode = concurrent_mode self._max_retry_times = max_retry_times self._is_print_detail_exception = is_print_detail_exception self._qps = qps if qps != 0: msg_schedule_time_intercal = 1.0 / qps # 使用qps覆盖消息调度间隔,以qps为准,以后废弃msg_schedule_time_intercal这个参数。 self._msg_schedule_time_intercal = msg_schedule_time_intercal if msg_schedule_time_intercal > 0.001 else 0.001 self._is_using_distributed_frequency_control = is_using_distributed_frequency_control self._is_send_consumer_hearbeat_to_redis = is_send_consumer_hearbeat_to_redis or is_using_distributed_frequency_control self._msg_expire_senconds = msg_expire_senconds if self._concurrent_mode not in (1, 2, 3, 4): raise ValueError('设置的并发模式不正确') self._concurrent_mode_dispatcher = ConcurrentModeDispatcher(self) if self._concurrent_mode == 4: self._run = self._async_run # 这里做了自动转化,使用async_run代替run self.__check_monkey_patch() self._logger_prefix = logger_prefix self._log_level = log_level if logger_prefix != '': logger_prefix += '--' # logger_name = f'{logger_prefix}{self.__class__.__name__}--{concurrent_name}--{queue_name}--{self.consuming_function.__name__}' logger_name = f'{logger_prefix}{self.__class__.__name__}--{queue_name}' # nb_print(logger_name) self.logger = LogManager(logger_name).get_logger_and_add_handlers(log_level, log_filename=f'{logger_name}.log' if create_logger_file else None, formatter_template=frame_config.NB_LOG_FORMATER_INDEX_FOR_CONSUMER_AND_PUBLISHER, ) # self.logger.info(f'{self.__class__} 在 {current_queue__info_dict["where_to_instantiate"]} 被实例化') stdout_write(f'{time.strftime("%H:%M:%S")} "{current_queue__info_dict["where_to_instantiate"]}" \033[0;30;44m此行 ' f'实例化队列名 {current_queue__info_dict["queue_name"]} 的消费者, 类型为 {self.__class__}\033[0m\n') self._do_task_filtering = do_task_filtering self._redis_filter_key_name = f'filter_zset:{queue_name}' if task_filtering_expire_seconds else f'filter_set:{queue_name}' filter_class = RedisFilter if task_filtering_expire_seconds == 0 else RedisImpermanencyFilter self._redis_filter = filter_class(self._redis_filter_key_name, task_filtering_expire_seconds) self._is_consuming_function_use_multi_params = is_consuming_function_use_multi_params self._unit_time_for_count = 10 # 每隔多少秒计数,显示单位时间内执行多少次,暂时固定为10秒。 self._execute_task_times_every_unit_time = 0 # 每单位时间执行了多少次任务。 self._lock_for_count_execute_task_times_every_unit_time = Lock() self._current_time_for_execute_task_times_every_unit_time = time.time() self._consuming_function_cost_time_total_every_unit_time = 0 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._result_persistence_helper = ResultPersistenceHelper(function_result_status_persistance_conf, queue_name) self._is_using_rpc_mode = is_using_rpc_mode self.stop_flag = False # 控频要用到的成员变量 self._last_submit_task_timestamp = 0 self._last_start_count_qps_timestamp = time.time() self._has_execute_times_in_recent_second = 0 self._publisher_of_same_queue = None self.consumer_identification = f'{socket.gethostname()}_{time_util.DatetimeConverter().datetime_str.replace(":", "-")}_{os.getpid()}_{id(self)}' self.custom_init() atexit.register(self.join_shedual_task_thread)
def show_all_consumer_info(cls): # nb_print(f'当前解释器内,所有消费者的信息是:\n {cls.consumers_queue__info_map}') nb_print(f'当前解释器内,所有消费者的信息是:\n {json.dumps(cls.consumers_queue__info_map, indent=4, ensure_ascii=False)}') for _, consumer_info in cls.consumers_queue__info_map.items(): stdout_write(f'{time.strftime("%H:%M:%S")} "{consumer_info["where_to_instantiate"]}" \033[0;30;44m{consumer_info["queue_name"]} 的消费者\033[0m\n')