funboost 49.6__py3-none-any.whl → 49.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of funboost might be problematic. Click here for more details.
- funboost/__init__.py +1 -1
- funboost/constant.py +19 -0
- funboost/consumers/base_consumer.py +27 -7
- funboost/consumers/faststream_consumer.py +1 -1
- funboost/consumers/http_consumer.py +12 -7
- funboost/consumers/kafka_consumer_manually_commit.py +0 -2
- funboost/consumers/tcp_consumer.py +11 -10
- funboost/consumers/udp_consumer.py +9 -6
- funboost/consumers/zeromq_consumer.py +18 -11
- funboost/core/exceptions.py +7 -0
- funboost/core/func_params_model.py +16 -7
- funboost/core/function_result_status_saver.py +15 -0
- funboost/core/msg_result_getter.py +51 -1
- funboost/core/serialization.py +28 -1
- funboost/factories/consumer_factory.py +1 -1
- funboost/factories/publisher_factotry.py +1 -1
- funboost/funboost_config_deafult.py +2 -2
- funboost/function_result_web/__pycache__/app.cpython-39.pyc +0 -0
- funboost/publishers/base_publisher.py +16 -2
- funboost/publishers/http_publisher.py +7 -1
- funboost/publishers/tcp_publisher.py +10 -8
- funboost/publishers/udp_publisher.py +8 -6
- funboost/publishers/zeromq_publisher.py +5 -1
- funboost/timing_job/timing_push.py +3 -1
- {funboost-49.6.dist-info → funboost-49.7.dist-info}/METADATA +165 -171
- {funboost-49.6.dist-info → funboost-49.7.dist-info}/RECORD +30 -30
- {funboost-49.6.dist-info → funboost-49.7.dist-info}/WHEEL +1 -1
- {funboost-49.6.dist-info → funboost-49.7.dist-info}/LICENSE +0 -0
- {funboost-49.6.dist-info → funboost-49.7.dist-info}/entry_points.txt +0 -0
- {funboost-49.6.dist-info → funboost-49.7.dist-info}/top_level.txt +0 -0
funboost/__init__.py
CHANGED
funboost/constant.py
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class BrokerEnum:
|
|
6
|
+
"""
|
|
7
|
+
在funboost中万物皆可为消息队列broker,funboost内置了所有 知名的正经经典消息队列作为broker,
|
|
8
|
+
也支持了基于 内存 各种数据库 文件系统 tcp/udp/http这些socket 模拟作为broker.
|
|
9
|
+
funboost也内置支持了各种python三方包和消费框架作为broker,例如 sqlachemy kombu celery rq dramtiq huey nameko 等等
|
|
10
|
+
|
|
11
|
+
用户也可以按照文档4.21章节,轻松扩展任何物质概念作为funboost的broker.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# funboost框架能轻松兼容消息队列各种工作模式, 拉模式/推模式/轮询模式,单条获取 批量获取
|
|
15
|
+
"""
|
|
16
|
+
funboost 的 consumer的 _shedual_task 非常灵活,用户实现把从消息队列取出的消息通过_submit_task方法
|
|
17
|
+
丢到并发池,他不是强制用户重写实现怎么取一条消息,例如强制你实现一个 _get_one_message的法,
|
|
18
|
+
那就不灵活和限制扩展任意东西作为broker了,而是用户完全自己来写灵活代码。
|
|
19
|
+
所以无论获取消息是 拉模式 还是推模式 还是轮询模式,是单条获取 还是多条批量获取,
|
|
20
|
+
不管你的新中间件和rabbitmq api用法差别有多么巨大,都能轻松扩展任意东西作为funboost的中间件。
|
|
21
|
+
所以你能看到funboost源码中能轻松实现任物质概念作为funboost的broker。
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
6
25
|
EMPTY = 'EMPTY' # 空的实现,需要搭配 boost入参的 consumer_override_cls 和 publisher_override_cls使用,或者被继承。
|
|
7
26
|
|
|
8
27
|
RABBITMQ_AMQPSTORM = 'RABBITMQ_AMQPSTORM' # 使用 amqpstorm 包操作rabbitmq 作为 分布式消息队列,支持消费确认.强烈推荐这个作为funboost中间件。
|
|
@@ -37,7 +37,7 @@ from funboost.core.current_task import funboost_current_task, FctContext
|
|
|
37
37
|
from funboost.core.loggers import develop_logger
|
|
38
38
|
|
|
39
39
|
from funboost.core.func_params_model import BoosterParams, PublisherParams, BaseJsonAbleModel
|
|
40
|
-
from funboost.core.serialization import Serialization
|
|
40
|
+
from funboost.core.serialization import PickleHelper, Serialization
|
|
41
41
|
from funboost.core.task_id_logger import TaskIdLogger
|
|
42
42
|
from funboost.constant import FunctionKind
|
|
43
43
|
|
|
@@ -448,10 +448,10 @@ class AbstractConsumer(LoggerLevelSetterMixin, metaclass=abc.ABCMeta, ):
|
|
|
448
448
|
"""
|
|
449
449
|
raise NotImplementedError
|
|
450
450
|
|
|
451
|
-
def
|
|
451
|
+
def _convert_msg_before_run(self, msg: typing.Union[str, dict]) -> dict:
|
|
452
452
|
"""
|
|
453
453
|
转换消息,消息没有使用funboost来发送,并且没有extra相关字段时候
|
|
454
|
-
用户也可以按照4.21文档,继承任意Consumer
|
|
454
|
+
用户也可以按照4.21文档,继承任意Consumer类,并实现方法 _user_convert_msg_before_run,先转换不规范的消息.
|
|
455
455
|
"""
|
|
456
456
|
""" 一般消息至少包含这样
|
|
457
457
|
{
|
|
@@ -469,6 +469,7 @@ class AbstractConsumer(LoggerLevelSetterMixin, metaclass=abc.ABCMeta, ):
|
|
|
469
469
|
extra_params = {'task_id': task_id, 'publish_time': round(time.time(), 4),
|
|
470
470
|
'publish_time_format': time.strftime('%Y-%m-%d %H:%M:%S')}
|
|
471
471
|
"""
|
|
472
|
+
msg = self._user_convert_msg_before_run(msg)
|
|
472
473
|
msg = Serialization.to_dict(msg)
|
|
473
474
|
# 以下是清洗补全字段.
|
|
474
475
|
if 'extra' not in msg:
|
|
@@ -481,10 +482,17 @@ class AbstractConsumer(LoggerLevelSetterMixin, metaclass=abc.ABCMeta, ):
|
|
|
481
482
|
if 'publish_time_format':
|
|
482
483
|
extra['publish_time_format'] = MsgGenerater.generate_publish_time_format()
|
|
483
484
|
return msg
|
|
485
|
+
|
|
486
|
+
def _user_convert_msg_before_run(self, msg: typing.Union[str, dict]) -> dict:
|
|
487
|
+
"""
|
|
488
|
+
用户也可以提前清洗数据
|
|
489
|
+
"""
|
|
490
|
+
# print(msg)
|
|
491
|
+
return msg
|
|
484
492
|
|
|
485
493
|
def _submit_task(self, kw):
|
|
486
494
|
|
|
487
|
-
kw['body'] = self.
|
|
495
|
+
kw['body'] = self._convert_msg_before_run(kw['body'])
|
|
488
496
|
self._print_message_get_from_broker(kw['body'])
|
|
489
497
|
if self._judge_is_daylight():
|
|
490
498
|
self._requeue(kw)
|
|
@@ -635,8 +643,9 @@ class AbstractConsumer(LoggerLevelSetterMixin, metaclass=abc.ABCMeta, ):
|
|
|
635
643
|
async def aio_user_custom_record_process_info_func(self, current_function_result_status: FunctionResultStatus): # 这个可以继承
|
|
636
644
|
pass
|
|
637
645
|
|
|
638
|
-
def _convert_real_function_only_params_by_conusuming_function_kind(self, function_only_params: dict):
|
|
646
|
+
def _convert_real_function_only_params_by_conusuming_function_kind(self, function_only_params: dict,extra_params:dict):
|
|
639
647
|
"""对于实例方法和classmethod 方法, 从消息队列的消息恢复第一个入参, self 和 cls"""
|
|
648
|
+
can_not_json_serializable_keys = extra_params.get('can_not_json_serializable_keys',[])
|
|
640
649
|
if self.consumer_params.consuming_function_kind in [FunctionKind.CLASS_METHOD, FunctionKind.INSTANCE_METHOD]:
|
|
641
650
|
real_function_only_params = copy.copy(function_only_params)
|
|
642
651
|
method_first_param_name = None
|
|
@@ -658,8 +667,14 @@ class AbstractConsumer(LoggerLevelSetterMixin, metaclass=abc.ABCMeta, ):
|
|
|
658
667
|
obj = method_cls(**method_first_param_value[ConstStrForClassMethod.OBJ_INIT_PARAMS])
|
|
659
668
|
real_function_only_params[method_first_param_name] = obj
|
|
660
669
|
# print(real_function_only_params)
|
|
670
|
+
if can_not_json_serializable_keys:
|
|
671
|
+
for key in can_not_json_serializable_keys:
|
|
672
|
+
real_function_only_params[key] = PickleHelper.to_obj(real_function_only_params[key])
|
|
661
673
|
return real_function_only_params
|
|
662
674
|
else:
|
|
675
|
+
if can_not_json_serializable_keys:
|
|
676
|
+
for key in can_not_json_serializable_keys:
|
|
677
|
+
function_only_params[key] = PickleHelper.to_obj(function_only_params[key])
|
|
663
678
|
return function_only_params
|
|
664
679
|
|
|
665
680
|
# noinspection PyProtectedMember
|
|
@@ -758,7 +773,7 @@ class AbstractConsumer(LoggerLevelSetterMixin, metaclass=abc.ABCMeta, ):
|
|
|
758
773
|
self.logger.warning(f'取消运行 {task_id} {function_only_params}')
|
|
759
774
|
return function_result_status
|
|
760
775
|
function_run = kill_remote_task.kill_fun_deco(task_id)(function_run) # 用杀死装饰器包装起来在另一个线程运行函数,以便等待远程杀死。
|
|
761
|
-
function_result_status.result = function_run(**self._convert_real_function_only_params_by_conusuming_function_kind(function_only_params))
|
|
776
|
+
function_result_status.result = function_run(**self._convert_real_function_only_params_by_conusuming_function_kind(function_only_params,kw['body']['extra']))
|
|
762
777
|
# if asyncio.iscoroutine(function_result_status.result):
|
|
763
778
|
# log_msg = f'''异步的协程消费函数必须使用 async 并发模式并发,请设置消费函数 {self.consuming_function.__name__} 的concurrent_mode 为 ConcurrentModeEnum.ASYNC 或 4'''
|
|
764
779
|
# # self.logger.critical(msg=f'{log_msg} \n')
|
|
@@ -813,6 +828,9 @@ class AbstractConsumer(LoggerLevelSetterMixin, metaclass=abc.ABCMeta, ):
|
|
|
813
828
|
self.logger.error(msg=log_msg, exc_info=self._get_priority_conf(kw, 'is_print_detail_exception'))
|
|
814
829
|
# traceback.print_exc()
|
|
815
830
|
function_result_status.exception = f'{e.__class__.__name__} {str(e)}'
|
|
831
|
+
function_result_status.exception_msg = str(e)
|
|
832
|
+
function_result_status.exception_type = e.__class__.__name__
|
|
833
|
+
|
|
816
834
|
function_result_status.result = FunctionResultStatus.FUNC_RUN_ERROR
|
|
817
835
|
return function_result_status
|
|
818
836
|
|
|
@@ -899,7 +917,7 @@ class AbstractConsumer(LoggerLevelSetterMixin, metaclass=abc.ABCMeta, ):
|
|
|
899
917
|
logger=self.logger,queue_name=self.queue_name,)
|
|
900
918
|
fct.set_fct_context(fct_context)
|
|
901
919
|
try:
|
|
902
|
-
corotinue_obj = self.consuming_function(**self._convert_real_function_only_params_by_conusuming_function_kind(function_only_params))
|
|
920
|
+
corotinue_obj = self.consuming_function(**self._convert_real_function_only_params_by_conusuming_function_kind(function_only_params,kw['body']['extra']))
|
|
903
921
|
if not asyncio.iscoroutine(corotinue_obj):
|
|
904
922
|
log_msg = f'''当前设置的并发模式为 async 并发模式,但消费函数不是异步协程函数,请不要把消费函数 {self.consuming_function.__name__} 的 concurrent_mode 设置错误'''
|
|
905
923
|
# self.logger.critical(msg=f'{log_msg} \n')
|
|
@@ -946,6 +964,8 @@ class AbstractConsumer(LoggerLevelSetterMixin, metaclass=abc.ABCMeta, ):
|
|
|
946
964
|
# self.error_file_logger.error(msg=f'{log_msg} \n', exc_info=self._get_priority_conf(kw, 'is_print_detail_exception'))
|
|
947
965
|
self.logger.error(msg=log_msg, exc_info=self._get_priority_conf(kw, 'is_print_detail_exception'))
|
|
948
966
|
function_result_status.exception = f'{e.__class__.__name__} {str(e)}'
|
|
967
|
+
function_result_status.exception_msg = str(e)
|
|
968
|
+
function_result_status.exception_type = e.__class__.__name__
|
|
949
969
|
function_result_status.result = FunctionResultStatus.FUNC_RUN_ERROR
|
|
950
970
|
return function_result_status
|
|
951
971
|
|
|
@@ -22,7 +22,7 @@ class FastStreamConsumer(EmptyConsumer):
|
|
|
22
22
|
# print(logger.name)
|
|
23
23
|
# return self.consuming_function(*args, **kwargs) # 如果没有声明 autoretry_for ,那么消费函数出错了就不会自动重试了。
|
|
24
24
|
# print(msg)
|
|
25
|
-
function_only_params = delete_keys_and_return_new_dict(
|
|
25
|
+
function_only_params = delete_keys_and_return_new_dict(Serialization.to_dict(msg))
|
|
26
26
|
if self._consuming_function_is_asyncio:
|
|
27
27
|
result = await self.consuming_function(**function_only_params)
|
|
28
28
|
else:
|
|
@@ -10,20 +10,25 @@ import json
|
|
|
10
10
|
from funboost.consumers.base_consumer import AbstractConsumer
|
|
11
11
|
from funboost.core.lazy_impoter import AioHttpImporter
|
|
12
12
|
|
|
13
|
+
|
|
13
14
|
class HTTPConsumer(AbstractConsumer, ):
|
|
14
15
|
"""
|
|
15
16
|
http 实现消息队列,不支持持久化,但不需要安装软件。
|
|
16
17
|
"""
|
|
17
|
-
|
|
18
|
+
BROKER_EXCLUSIVE_CONFIG_DEFAULT = {'host': '127.0.0.1', 'port': None}
|
|
18
19
|
|
|
19
20
|
# noinspection PyAttributeOutsideInit
|
|
20
21
|
def custom_init(self):
|
|
21
|
-
try:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
except BaseException as e:
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
# try:
|
|
23
|
+
# self._ip, self._port = self.queue_name.split(':')
|
|
24
|
+
# self._port = int(self._port)
|
|
25
|
+
# except BaseException as e:
|
|
26
|
+
# self.logger.critical(f'http作为消息队列时候,队列名字必须设置为 例如 192.168.1.101:8200 这种, ip:port')
|
|
27
|
+
# raise e
|
|
28
|
+
self._ip = self.consumer_params.broker_exclusive_config['host']
|
|
29
|
+
self._port = self.consumer_params.broker_exclusive_config['port']
|
|
30
|
+
if self._port is None:
|
|
31
|
+
raise ValueError('please specify port')
|
|
27
32
|
|
|
28
33
|
# noinspection DuplicatedCode
|
|
29
34
|
def _shedual_task(self):
|
|
@@ -72,8 +72,6 @@ class KafkaConsumerManuallyCommit(AbstractConsumer):
|
|
|
72
72
|
f'从kafka的 [{self._queue_name}] 主题,分区 {msg.partition()} 中 的 offset {msg.offset()} 取出的消息是: {msg.value()}') # noqa
|
|
73
73
|
self._submit_task(kw)
|
|
74
74
|
|
|
75
|
-
# kw = {'consumer': consumer, 'message': message, 'body': json.loads(message.value)}
|
|
76
|
-
# self._submit_task(kw)
|
|
77
75
|
|
|
78
76
|
def _manually_commit(self):
|
|
79
77
|
"""
|
|
@@ -13,32 +13,34 @@ class TCPConsumer(AbstractConsumer, ):
|
|
|
13
13
|
socket 实现消息队列,不支持持久化,但不需要安装软件。
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
BUFSIZE = 10240
|
|
16
|
+
BROKER_EXCLUSIVE_CONFIG_DEFAULT = {'host': '127.0.0.1', 'port': None, 'bufsize': 10240}
|
|
18
17
|
|
|
19
18
|
# noinspection PyAttributeOutsideInit
|
|
20
19
|
def custom_init(self):
|
|
21
|
-
ip__port_str = self.queue_name.split(':')
|
|
22
|
-
ip_port = (ip__port_str[0], int(ip__port_str[1]))
|
|
23
|
-
self._ip_port_raw = ip_port
|
|
24
|
-
self._ip_port = ('', ip_port[1])
|
|
20
|
+
# ip__port_str = self.queue_name.split(':')
|
|
21
|
+
# ip_port = (ip__port_str[0], int(ip__port_str[1]))
|
|
22
|
+
# self._ip_port_raw = ip_port
|
|
23
|
+
# self._ip_port = ('', ip_port[1])
|
|
25
24
|
# ip_port = ('', 9999)
|
|
25
|
+
self._ip_port = (self.consumer_params.broker_exclusive_config['host'],
|
|
26
|
+
self.consumer_params.broker_exclusive_config['port'])
|
|
27
|
+
self.bufsize = self.consumer_params.broker_exclusive_config['bufsize']
|
|
26
28
|
|
|
27
29
|
# noinspection DuplicatedCode
|
|
28
30
|
def _shedual_task(self):
|
|
29
|
-
|
|
31
|
+
|
|
30
32
|
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # tcp协议
|
|
31
33
|
server.bind(self._ip_port)
|
|
32
34
|
server.listen(128)
|
|
33
35
|
self._server = server
|
|
34
36
|
while True:
|
|
35
37
|
tcp_cli_sock, addr = self._server.accept()
|
|
36
|
-
Thread(target=self.__handle_conn, args=(tcp_cli_sock,)).start()
|
|
38
|
+
Thread(target=self.__handle_conn, args=(tcp_cli_sock,)).start() # 服务端多线程,可以同时处理多个tcp长链接客户端发来的消息。
|
|
37
39
|
|
|
38
40
|
def __handle_conn(self, tcp_cli_sock):
|
|
39
41
|
try:
|
|
40
42
|
while True:
|
|
41
|
-
data = tcp_cli_sock.recv(self.
|
|
43
|
+
data = tcp_cli_sock.recv(self.bufsize)
|
|
42
44
|
# print('server收到的数据', data)
|
|
43
45
|
if not data:
|
|
44
46
|
break
|
|
@@ -56,4 +58,3 @@ class TCPConsumer(AbstractConsumer, ):
|
|
|
56
58
|
|
|
57
59
|
def _requeue(self, kw):
|
|
58
60
|
pass
|
|
59
|
-
|
|
@@ -12,14 +12,17 @@ class UDPConsumer(AbstractConsumer, ):
|
|
|
12
12
|
socket 实现消息队列,不支持持久化,但不需要安装软件。
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
BROKER_EXCLUSIVE_CONFIG_DEFAULT = {'host': '127.0.0.1', 'port': None, 'bufsize': 10240}
|
|
16
16
|
|
|
17
17
|
# noinspection PyAttributeOutsideInit
|
|
18
18
|
def custom_init(self):
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
self.__udp_client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
21
|
-
ip__port_str = self.queue_name.split(':')
|
|
22
|
-
self.__ip_port = (ip__port_str[0], int(ip__port_str[1]))
|
|
21
|
+
# ip__port_str = self.queue_name.split(':')
|
|
22
|
+
# self.__ip_port = (ip__port_str[0], int(ip__port_str[1]))
|
|
23
|
+
self.__ip_port = (self.consumer_params.broker_exclusive_config['host'],
|
|
24
|
+
self.consumer_params.broker_exclusive_config['port'])
|
|
25
|
+
self._bufsize = self.consumer_params.broker_exclusive_config['bufsize']
|
|
23
26
|
self.__udp_client.connect(self.__ip_port)
|
|
24
27
|
|
|
25
28
|
# noinspection DuplicatedCode
|
|
@@ -29,7 +32,7 @@ class UDPConsumer(AbstractConsumer, ):
|
|
|
29
32
|
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # udp协议
|
|
30
33
|
server.bind(ip_port)
|
|
31
34
|
while True:
|
|
32
|
-
data, client_addr = server.recvfrom(self.
|
|
35
|
+
data, client_addr = server.recvfrom(self._bufsize)
|
|
33
36
|
# print('server收到的数据', data)
|
|
34
37
|
# self._print_message_get_from_broker(f'udp {ip_port}', data.decode())
|
|
35
38
|
server.sendto('has_recived'.encode(), client_addr)
|
|
@@ -41,4 +44,4 @@ class UDPConsumer(AbstractConsumer, ):
|
|
|
41
44
|
|
|
42
45
|
def _requeue(self, kw):
|
|
43
46
|
self.__udp_client.send(json.dumps(kw['body']).encode())
|
|
44
|
-
data = self.__udp_client.recv(self.
|
|
47
|
+
data = self.__udp_client.recv(self._bufsize)
|
|
@@ -67,31 +67,38 @@ class ZeroMqConsumer(AbstractConsumer):
|
|
|
67
67
|
zeromq 中间件的消费者,zeromq基于socket代码,不会持久化,且不需要安装软件。
|
|
68
68
|
"""
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
BROKER_EXCLUSIVE_CONFIG_DEFAULT = {'port': None}
|
|
71
|
+
|
|
72
|
+
def custom_init(self):
|
|
73
|
+
self._port = self.consumer_params.broker_exclusive_config['port']
|
|
74
|
+
if self._port is None:
|
|
75
|
+
raise ValueError('please specify port')
|
|
76
|
+
|
|
77
|
+
def _start_broker_port(self):
|
|
71
78
|
# threading.Thread(target=self._start_broker).start()
|
|
72
79
|
# noinspection PyBroadException
|
|
73
80
|
try:
|
|
74
|
-
if not (10000 < int(self.
|
|
75
|
-
raise ValueError("
|
|
81
|
+
if not (10000 < int(self._port) < 65535):
|
|
82
|
+
raise ValueError("请设置port是一个 10000到65535的之间的一个端口数字")
|
|
76
83
|
except BaseException:
|
|
77
|
-
self.logger.critical(f"
|
|
84
|
+
self.logger.critical(f" 请设置port是一个 10000到65535的之间的一个端口数字")
|
|
78
85
|
# noinspection PyProtectedMember
|
|
79
86
|
os._exit(444)
|
|
80
|
-
if check_port_is_used('127.0.0.1', int(self.
|
|
81
|
-
self.logger.debug(f"""{int(self.
|
|
87
|
+
if check_port_is_used('127.0.0.1', int(self._port)):
|
|
88
|
+
self.logger.debug(f"""{int(self._port)} router端口已经启动(或占用) """)
|
|
82
89
|
return
|
|
83
|
-
if check_port_is_used('127.0.0.1', int(self.
|
|
84
|
-
self.logger.debug(f"""{int(self.
|
|
90
|
+
if check_port_is_used('127.0.0.1', int(self._port) + 1):
|
|
91
|
+
self.logger.debug(f"""{int(self._port) + 1} dealer 端口已经启动(或占用) """)
|
|
85
92
|
return
|
|
86
|
-
multiprocessing.Process(target=start_broker, args=(int(self.
|
|
93
|
+
multiprocessing.Process(target=start_broker, args=(int(self._port), int(self._port) + 1)).start()
|
|
87
94
|
|
|
88
95
|
# noinspection DuplicatedCode
|
|
89
96
|
def _shedual_task(self):
|
|
90
|
-
self.
|
|
97
|
+
self._start_broker_port()
|
|
91
98
|
context = ZmqImporter().zmq.Context()
|
|
92
99
|
# noinspection PyUnresolvedReferences
|
|
93
100
|
zsocket = context.socket(ZmqImporter().zmq.REP)
|
|
94
|
-
zsocket.connect(f"tcp://localhost:{int(self.
|
|
101
|
+
zsocket.connect(f"tcp://localhost:{int(self._port) + 1}")
|
|
95
102
|
|
|
96
103
|
while True:
|
|
97
104
|
message = zsocket.recv()
|
funboost/core/exceptions.py
CHANGED
|
@@ -11,6 +11,11 @@ class ExceptionForRetry(FunboostException):
|
|
|
11
11
|
class ExceptionForRequeue(FunboostException):
|
|
12
12
|
"""框架检测到此错误,重新放回当前队列中"""
|
|
13
13
|
|
|
14
|
+
class FunboostWaitRpcResultTimeout(FunboostException):
|
|
15
|
+
"""等待rpc结果超过了指定时间"""
|
|
16
|
+
|
|
17
|
+
class FunboostRpcResultError(FunboostException):
|
|
18
|
+
"""rpc结果是错误状态"""
|
|
14
19
|
|
|
15
20
|
class ExceptionForPushToDlxqueue(FunboostException):
|
|
16
21
|
"""框架检测到ExceptionForPushToDlxqueue错误,发布到死信队列"""
|
|
@@ -40,3 +45,5 @@ def f(x):
|
|
|
40
45
|
|
|
41
46
|
def __str__(self):
|
|
42
47
|
return self.new_version_change_hint
|
|
48
|
+
|
|
49
|
+
|
|
@@ -109,8 +109,8 @@ class FunctionResultStatusPersistanceConfig(BaseJsonAbleModel):
|
|
|
109
109
|
flogger.warning(f'你设置的过期时间为 {value} ,设置的时间过长。 ')
|
|
110
110
|
return value
|
|
111
111
|
|
|
112
|
-
@root_validator(skip_on_failure=True
|
|
113
|
-
def
|
|
112
|
+
@root_validator(skip_on_failure=True)
|
|
113
|
+
def check_values(cls, values: dict):
|
|
114
114
|
if not values['is_save_status'] and values['is_save_result']:
|
|
115
115
|
raise ValueError(f'你设置的是不保存函数运行状态但保存函数运行结果。不允许你这么设置')
|
|
116
116
|
return values
|
|
@@ -159,10 +159,18 @@ class BoosterParams(BaseJsonAbleModel):
|
|
|
159
159
|
consumin_function_decorator: typing.Optional[typing.Callable] = None # 函数的装饰器。因为此框架做参数自动转指点,需要获取精准的入参名称,不支持在消费函数上叠加 @ *args **kwargs的装饰器,如果想用装饰器可以这里指定。
|
|
160
160
|
function_timeout: typing.Union[int, float,None] = None # 超时秒数,函数运行超过这个时间,则自动杀死函数。为0是不限制。 谨慎使用,非必要别去设置超时时间,设置后性能会降低(因为需要把用户函数包装到另一个线单独的程中去运行),而且突然强制超时杀死运行中函数,可能会造成死锁.(例如用户函数在获得线程锁后突然杀死函数,别的线程再也无法获得锁了)
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
"""
|
|
163
|
+
log_level:
|
|
164
|
+
logger_name 对应的 日志级别
|
|
165
|
+
消费者和发布者的日志级别,建议设置DEBUG级别,不然无法知道正在运行什么消息.
|
|
166
|
+
这个是funboost每个队列的单独命名空间的日志级别,丝毫不会影响改变用户其他日志以及root命名空间的日志级别,所以DEBUG级别就好,
|
|
167
|
+
用户不要压根不懂什么是python logger 的name,还去手痒调高级别.
|
|
168
|
+
不懂python日志命名空间的小白去看nb_log文档,或者直接问 ai大模型 python logger name的作用是什么.
|
|
169
|
+
"""
|
|
170
|
+
log_level: int = logging.DEBUG # 不需要改这个级别,请看上面原因
|
|
163
171
|
logger_prefix: str = '' # 日志名字前缀,可以设置前缀
|
|
164
172
|
create_logger_file: bool = True # 发布者和消费者是否创建文件文件日志,为False则只打印控制台不写文件.
|
|
165
|
-
logger_name: str = '' # 队列消费者发布者的日志命名空间.
|
|
173
|
+
logger_name: typing.Union[str, None] = '' # 队列消费者发布者的日志命名空间.
|
|
166
174
|
log_filename: typing.Union[str, None] = None # 消费者发布者的文件日志名字.如果为None,则自动使用 funboost.队列 名字作为文件日志名字. 日志文件夹是在nb_log_config.py的 LOG_PATH中决定的.
|
|
167
175
|
is_show_message_get_from_broker: bool = False # 运行时候,是否记录从消息队列获取出来的消息内容
|
|
168
176
|
is_print_detail_exception: bool = True # 消费函数出错时候,是否打印详细的报错堆栈,为False则只打印简略的报错信息不包含堆栈.
|
|
@@ -221,7 +229,7 @@ class BoosterParams(BaseJsonAbleModel):
|
|
|
221
229
|
|
|
222
230
|
|
|
223
231
|
|
|
224
|
-
@root_validator(skip_on_failure=True)
|
|
232
|
+
@root_validator(skip_on_failure=True, )
|
|
225
233
|
def check_values(cls, values: dict):
|
|
226
234
|
|
|
227
235
|
|
|
@@ -307,7 +315,7 @@ class PriorityConsumingControlConfig(BaseJsonAbleModel):
|
|
|
307
315
|
eta: typing.Union[datetime.datetime, str,None] = None # 时间对象, 或 %Y-%m-%d %H:%M:%S 字符串。
|
|
308
316
|
misfire_grace_time: typing.Union[int, None] = None
|
|
309
317
|
|
|
310
|
-
other_extra_params: dict = None # 其他参数, 例如消息优先级 , priority_control_config=PriorityConsumingControlConfig(other_extra_params={'priroty': priorityxx}),
|
|
318
|
+
other_extra_params: typing.Optional[dict] = None # 其他参数, 例如消息优先级 , priority_control_config=PriorityConsumingControlConfig(other_extra_params={'priroty': priorityxx}),
|
|
311
319
|
|
|
312
320
|
"""filter_str:
|
|
313
321
|
用户指定过滤字符串, 例如函数入参是 def fun(userid,username,sex,user_description),
|
|
@@ -318,6 +326,7 @@ class PriorityConsumingControlConfig(BaseJsonAbleModel):
|
|
|
318
326
|
"""
|
|
319
327
|
filter_str :typing.Optional[str] = None
|
|
320
328
|
|
|
329
|
+
can_not_json_serializable_keys: typing.List[str] = None # 不能json序列化的入参名字,反序列化时候需要使用pickle来反序列化这些字段(这个是自动生成的,用户不需要手动指定此入参。)
|
|
321
330
|
@root_validator(skip_on_failure=True)
|
|
322
331
|
def cehck_values(cls, values: dict):
|
|
323
332
|
if values['countdown'] and values['eta']:
|
|
@@ -356,4 +365,4 @@ if __name__ == '__main__':
|
|
|
356
365
|
# print(PriorityConsumingControlConfig().get_str_dict())
|
|
357
366
|
|
|
358
367
|
print(BoosterParams(queue_name='3213', specify_concurrent_pool=FlexibleThreadPool(100)).json_pre())
|
|
359
|
-
print(PublisherParams.schema_json())
|
|
368
|
+
# print(PublisherParams.schema_json()) # 注释掉,因为 PublisherParams 包含 Callable 类型字段,无法生成 JSON Schema
|
|
@@ -51,6 +51,9 @@ class FunctionResultStatus():
|
|
|
51
51
|
self.result = None
|
|
52
52
|
self.run_times = 0
|
|
53
53
|
self.exception = None
|
|
54
|
+
self.exception_type = None
|
|
55
|
+
self.exception_msg = None
|
|
56
|
+
self.rpc_chain_error_msg_dict:dict = None
|
|
54
57
|
self.time_start = time.time()
|
|
55
58
|
self.time_cost = None
|
|
56
59
|
self.time_end = None
|
|
@@ -62,6 +65,15 @@ class FunctionResultStatus():
|
|
|
62
65
|
self._has_kill_task = False
|
|
63
66
|
self.rpc_result_expire_seconds = None
|
|
64
67
|
|
|
68
|
+
@classmethod
|
|
69
|
+
def parse_status_and_result_to_obj(cls,status_dict:dict):
|
|
70
|
+
obj = cls(status_dict['queue_name'],status_dict['function'],status_dict['msg_dict'])
|
|
71
|
+
for k,v in status_dict.items():
|
|
72
|
+
# if k.startswith('_'):
|
|
73
|
+
# continue
|
|
74
|
+
setattr(obj,k,v)
|
|
75
|
+
return obj
|
|
76
|
+
|
|
65
77
|
def get_status_dict(self, without_datetime_obj=False):
|
|
66
78
|
self.time_end = time.time()
|
|
67
79
|
if self.run_status == RunStatus.running:
|
|
@@ -102,6 +114,9 @@ class FunctionResultStatus():
|
|
|
102
114
|
def __str__(self):
|
|
103
115
|
return f'''{self.__class__} {Serialization.to_json_str(self.get_status_dict())}'''
|
|
104
116
|
|
|
117
|
+
def to_pretty_json_str(self):
|
|
118
|
+
return json.dumps(self.get_status_dict(),indent=4,ensure_ascii=False)
|
|
119
|
+
|
|
105
120
|
|
|
106
121
|
class ResultPersistenceHelper(MongoMixin, FunboostFileLoggerMixin):
|
|
107
122
|
TASK_STATUS_DB = 'task_status'
|
|
@@ -12,13 +12,29 @@ from funboost.utils.redis_manager import RedisMixin
|
|
|
12
12
|
from funboost.utils.redis_manager import AioRedisMixin
|
|
13
13
|
from funboost.core.serialization import Serialization
|
|
14
14
|
|
|
15
|
+
from funboost.core.function_result_status_saver import FunctionResultStatus
|
|
16
|
+
|
|
15
17
|
class HasNotAsyncResult(Exception):
|
|
16
18
|
pass
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
NO_RESULT = 'no_result'
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
def _judge_rpc_function_result_status_obj(status_and_result_obj:FunctionResultStatus,raise_exception:bool):
|
|
24
|
+
if status_and_result_obj is None:
|
|
25
|
+
raise FunboostWaitRpcResultTimeout(f'等待 {status_and_result_obj.task_id} rpc结果超过了指定时间')
|
|
26
|
+
if status_and_result_obj.success is True:
|
|
27
|
+
return status_and_result_obj
|
|
28
|
+
else:
|
|
29
|
+
raw_erorr = status_and_result_obj.exception
|
|
30
|
+
if status_and_result_obj.exception_type == 'FunboostRpcResultError':
|
|
31
|
+
raw_erorr = json.loads(status_and_result_obj.exception_msg) # 使canvas链式报错json显示更美观
|
|
32
|
+
error_msg_dict = {'task_id':status_and_result_obj.task_id,'raw_error':raw_erorr}
|
|
33
|
+
if raise_exception:
|
|
34
|
+
raise FunboostRpcResultError(json.dumps(error_msg_dict,indent=4,ensure_ascii=False))
|
|
35
|
+
else:
|
|
36
|
+
status_and_result_obj.rpc_chain_error_msg_dict = error_msg_dict
|
|
37
|
+
return status_and_result_obj
|
|
22
38
|
class AsyncResult(RedisMixin):
|
|
23
39
|
default_callback_run_executor = FlexibleThreadPoolMinWorkers0(200,work_queue_maxsize=50)
|
|
24
40
|
|
|
@@ -63,6 +79,14 @@ class AsyncResult(RedisMixin):
|
|
|
63
79
|
return self._status_and_result
|
|
64
80
|
return None
|
|
65
81
|
return self._status_and_result
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def status_and_result_obj(self) -> FunctionResultStatus:
|
|
85
|
+
"""这个是为了比字典有更好的ide代码补全效果"""
|
|
86
|
+
if self.status_and_result is not None:
|
|
87
|
+
return FunctionResultStatus.parse_status_and_result_to_obj(self.status_and_result)
|
|
88
|
+
|
|
89
|
+
rpc_data =status_and_result_obj
|
|
66
90
|
|
|
67
91
|
def get(self):
|
|
68
92
|
# print(self.status_and_result)
|
|
@@ -104,6 +128,14 @@ class AsyncResult(RedisMixin):
|
|
|
104
128
|
async_result.set_callback(show_result) # 使用回调函数在线程池中并发的运行函数结果
|
|
105
129
|
'''
|
|
106
130
|
self.callback_run_executor.submit(self._run_callback_func, callback_func)
|
|
131
|
+
|
|
132
|
+
def wait_rpc_data_or_raise(self,raise_exception:bool=True)->FunctionResultStatus:
|
|
133
|
+
return _judge_rpc_function_result_status_obj(self.status_and_result_obj,raise_exception)
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def batch_wait_rpc_data_or_raise(cls,r_list:typing.List['AsyncResult'],raise_exception:bool=True)->typing.List[FunctionResultStatus]:
|
|
137
|
+
return [ _judge_rpc_function_result_status_obj(r.status_and_result_obj,raise_exception)
|
|
138
|
+
for r in r_list]
|
|
107
139
|
|
|
108
140
|
|
|
109
141
|
class AioAsyncResult(AioRedisMixin):
|
|
@@ -169,6 +201,14 @@ if __name__ == '__main__':
|
|
|
169
201
|
return None
|
|
170
202
|
return self._status_and_result
|
|
171
203
|
|
|
204
|
+
@property
|
|
205
|
+
async def status_and_result_obj(self) -> FunctionResultStatus:
|
|
206
|
+
"""这个是为了比字典有更好的ide代码补全效果"""
|
|
207
|
+
sr = await self.status_and_result
|
|
208
|
+
if sr is not None:
|
|
209
|
+
return FunctionResultStatus.parse_status_and_result_to_obj(sr)
|
|
210
|
+
|
|
211
|
+
rpc_data =status_and_result_obj
|
|
172
212
|
async def get(self):
|
|
173
213
|
# print(self.status_and_result)
|
|
174
214
|
if (await self.status_and_result) is not None:
|
|
@@ -192,6 +232,16 @@ if __name__ == '__main__':
|
|
|
192
232
|
async def set_callback(self, aio_callback_func: typing.Callable):
|
|
193
233
|
asyncio.create_task(self._run_callback_func(callback_func=aio_callback_func))
|
|
194
234
|
|
|
235
|
+
async def wait_rpc_data_or_raise(self,raise_exception:bool=True)->FunctionResultStatus:
|
|
236
|
+
return _judge_rpc_function_result_status_obj(await self.status_and_result_obj,raise_exception)
|
|
237
|
+
|
|
238
|
+
@classmethod
|
|
239
|
+
async def batch_wait_rpc_data_or_raise(cls,r_list:typing.List['AioAsyncResult'],raise_exception:bool=True)->typing.List[FunctionResultStatus]:
|
|
240
|
+
return [ _judge_rpc_function_result_status_obj(await r.status_and_result_obj,raise_exception)
|
|
241
|
+
for r in r_list]
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
|
|
195
245
|
|
|
196
246
|
class ResultFromMongo(MongoMixin):
|
|
197
247
|
"""
|
funboost/core/serialization.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import typing
|
|
2
|
-
|
|
2
|
+
import json
|
|
3
3
|
import orjson
|
|
4
|
+
import pickle
|
|
5
|
+
import ast
|
|
6
|
+
|
|
4
7
|
class Serialization:
|
|
5
8
|
@staticmethod
|
|
6
9
|
def to_json_str(dic:typing.Union[dict,str]):
|
|
@@ -14,3 +17,27 @@ class Serialization:
|
|
|
14
17
|
if isinstance(strx,dict):
|
|
15
18
|
return strx
|
|
16
19
|
return orjson.loads(strx)
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def find_can_not_json_serializable_keys(dic:dict)->typing.List[str]:
|
|
23
|
+
can_not_json_serializable_keys = []
|
|
24
|
+
dic = Serialization.to_dict(dic)
|
|
25
|
+
for k,v in dic.items():
|
|
26
|
+
if not isinstance(v,str):
|
|
27
|
+
try:
|
|
28
|
+
json.dumps(v)
|
|
29
|
+
except:
|
|
30
|
+
can_not_json_serializable_keys.append(k)
|
|
31
|
+
return can_not_json_serializable_keys
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PickleHelper:
|
|
35
|
+
@staticmethod
|
|
36
|
+
def to_str(obj_x:typing.Any):
|
|
37
|
+
return str(pickle.dumps(obj_x)) # 对象pickle,转成字符串
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def to_obj(str_x:str):
|
|
41
|
+
return pickle.loads(ast.literal_eval(str_x)) # 不是从字节转成对象,是从字符串转,所以需要这样.
|
|
42
|
+
|
|
43
|
+
|
|
@@ -19,7 +19,7 @@ def get_consumer(boost_params: BoosterParams) -> AbstractConsumer:
|
|
|
19
19
|
regist_to_funboost(boost_params.broker_kind) # 动态注册中间件到框架是为了延迟导入,用户没安装不需要的第三方包不报错。
|
|
20
20
|
|
|
21
21
|
if boost_params.broker_kind not in broker_kind__publsiher_consumer_type_map:
|
|
22
|
-
raise ValueError(f'
|
|
22
|
+
raise ValueError(f'设置的中间件种类不正确,你设置的值是 {boost_params.broker_kind} ')
|
|
23
23
|
consumer_cls = broker_kind__publsiher_consumer_type_map[boost_params.broker_kind][1]
|
|
24
24
|
if not boost_params.consumer_override_cls:
|
|
25
25
|
return consumer_cls(boost_params)
|
|
@@ -31,7 +31,7 @@ def get_publisher(publisher_params: PublisherParams) -> AbstractPublisher:
|
|
|
31
31
|
broker_kind = publisher_params.broker_kind
|
|
32
32
|
regist_to_funboost(broker_kind) # 动态注册中间件到框架是为了延迟导入,用户没安装不需要的第三方包不报错。
|
|
33
33
|
if broker_kind not in broker_kind__publsiher_consumer_type_map:
|
|
34
|
-
raise ValueError(f'
|
|
34
|
+
raise ValueError(f'设置的中间件种类不正确,你设置的值是 {broker_kind} ')
|
|
35
35
|
publisher_cls = broker_kind__publsiher_consumer_type_map[broker_kind][0]
|
|
36
36
|
if not publisher_params.publisher_override_cls:
|
|
37
37
|
return publisher_cls(publisher_params)
|
|
@@ -44,7 +44,7 @@ class BrokerConnConfig(DataClassBase):
|
|
|
44
44
|
REDIS_PORT = 6379
|
|
45
45
|
REDIS_DB = 7 # redis消息队列所在db,请不要在这个db放太多其他键值对,以及方便你自己可视化查看你的redis db,框架里面有的功能会scan扫描unacked的键名,使用单独的db。
|
|
46
46
|
REDIS_DB_FILTER_AND_RPC_RESULT = 8 # 如果函数做任务参数过滤 或者使用rpc获取结果,使用这个db,因为这个db的键值对多,和redis消息队列db分开
|
|
47
|
-
REDIS_SSL = False
|
|
47
|
+
REDIS_SSL = False # 是否使用ssl加密,默认是False
|
|
48
48
|
REDIS_URL = f'{"rediss" if REDIS_SSL else "redis"}://{REDIS_USERNAME}:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}'
|
|
49
49
|
|
|
50
50
|
NSQD_TCP_ADDRESSES = ['127.0.0.1:4150']
|
|
@@ -80,7 +80,7 @@ class BrokerConnConfig(DataClassBase):
|
|
|
80
80
|
MQTT_TCP_PORT = 1883
|
|
81
81
|
|
|
82
82
|
HTTPSQS_HOST = '127.0.0.1'
|
|
83
|
-
HTTPSQS_PORT =
|
|
83
|
+
HTTPSQS_PORT = 1218
|
|
84
84
|
HTTPSQS_AUTH = '123456'
|
|
85
85
|
|
|
86
86
|
NATS_URL = 'nats://192.168.6.134:4222'
|
|
Binary file
|