jettask 0.2.20__py3-none-any.whl → 0.2.24__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.
- jettask/__init__.py +4 -0
- jettask/cli.py +12 -8
- jettask/config/lua_scripts.py +37 -0
- jettask/config/nacos_config.py +1 -1
- jettask/core/app.py +313 -340
- jettask/core/container.py +4 -4
- jettask/{persistence → core}/namespace.py +93 -27
- jettask/core/task.py +16 -9
- jettask/core/unified_manager_base.py +136 -26
- jettask/db/__init__.py +67 -0
- jettask/db/base.py +137 -0
- jettask/{utils/db_connector.py → db/connector.py} +130 -26
- jettask/db/models/__init__.py +16 -0
- jettask/db/models/scheduled_task.py +196 -0
- jettask/db/models/task.py +77 -0
- jettask/db/models/task_run.py +85 -0
- jettask/executor/__init__.py +0 -15
- jettask/executor/core.py +76 -31
- jettask/executor/process_entry.py +29 -114
- jettask/executor/task_executor.py +4 -0
- jettask/messaging/event_pool.py +928 -685
- jettask/messaging/scanner.py +30 -0
- jettask/persistence/__init__.py +28 -103
- jettask/persistence/buffer.py +170 -0
- jettask/persistence/consumer.py +330 -249
- jettask/persistence/manager.py +304 -0
- jettask/persistence/persistence.py +391 -0
- jettask/scheduler/__init__.py +15 -3
- jettask/scheduler/{task_crud.py → database.py} +61 -57
- jettask/scheduler/loader.py +2 -2
- jettask/scheduler/{scheduler_coordinator.py → manager.py} +23 -6
- jettask/scheduler/models.py +14 -10
- jettask/scheduler/schedule.py +166 -0
- jettask/scheduler/scheduler.py +12 -11
- jettask/schemas/__init__.py +50 -1
- jettask/schemas/backlog.py +43 -6
- jettask/schemas/namespace.py +70 -19
- jettask/schemas/queue.py +19 -3
- jettask/schemas/responses.py +493 -0
- jettask/task/__init__.py +0 -2
- jettask/task/router.py +3 -0
- jettask/test_connection_monitor.py +1 -1
- jettask/utils/__init__.py +7 -5
- jettask/utils/db_init.py +8 -4
- jettask/utils/namespace_dep.py +167 -0
- jettask/utils/queue_matcher.py +186 -0
- jettask/utils/rate_limit/concurrency_limiter.py +7 -1
- jettask/utils/stream_backlog.py +1 -1
- jettask/webui/__init__.py +0 -1
- jettask/webui/api/__init__.py +4 -4
- jettask/webui/api/alerts.py +806 -71
- jettask/webui/api/example_refactored.py +400 -0
- jettask/webui/api/namespaces.py +390 -45
- jettask/webui/api/overview.py +300 -54
- jettask/webui/api/queues.py +971 -267
- jettask/webui/api/scheduled.py +1249 -56
- jettask/webui/api/settings.py +129 -7
- jettask/webui/api/workers.py +442 -0
- jettask/webui/app.py +46 -2329
- jettask/webui/middleware/__init__.py +6 -0
- jettask/webui/middleware/namespace_middleware.py +135 -0
- jettask/webui/services/__init__.py +146 -0
- jettask/webui/services/heartbeat_service.py +251 -0
- jettask/webui/services/overview_service.py +60 -51
- jettask/webui/services/queue_monitor_service.py +426 -0
- jettask/webui/services/redis_monitor_service.py +87 -0
- jettask/webui/services/settings_service.py +174 -111
- jettask/webui/services/task_monitor_service.py +222 -0
- jettask/webui/services/timeline_pg_service.py +452 -0
- jettask/webui/services/timeline_service.py +189 -0
- jettask/webui/services/worker_monitor_service.py +467 -0
- jettask/webui/utils/__init__.py +11 -0
- jettask/webui/utils/time_utils.py +122 -0
- jettask/worker/lifecycle.py +8 -2
- {jettask-0.2.20.dist-info → jettask-0.2.24.dist-info}/METADATA +1 -1
- jettask-0.2.24.dist-info/RECORD +142 -0
- jettask/executor/executor.py +0 -338
- jettask/persistence/backlog_monitor.py +0 -567
- jettask/persistence/base.py +0 -2334
- jettask/persistence/db_manager.py +0 -516
- jettask/persistence/maintenance.py +0 -81
- jettask/persistence/message_consumer.py +0 -259
- jettask/persistence/models.py +0 -49
- jettask/persistence/offline_recovery.py +0 -196
- jettask/persistence/queue_discovery.py +0 -215
- jettask/persistence/task_persistence.py +0 -218
- jettask/persistence/task_updater.py +0 -583
- jettask/scheduler/add_execution_count.sql +0 -11
- jettask/scheduler/add_priority_field.sql +0 -26
- jettask/scheduler/add_scheduler_id.sql +0 -25
- jettask/scheduler/add_scheduler_id_index.sql +0 -10
- jettask/scheduler/make_scheduler_id_required.sql +0 -28
- jettask/scheduler/migrate_interval_seconds.sql +0 -9
- jettask/scheduler/performance_optimization.sql +0 -45
- jettask/scheduler/run_scheduler.py +0 -186
- jettask/scheduler/schema.sql +0 -84
- jettask/task/task_executor.py +0 -318
- jettask/webui/api/analytics.py +0 -323
- jettask/webui/config.py +0 -90
- jettask/webui/models/__init__.py +0 -3
- jettask/webui/models/namespace.py +0 -63
- jettask/webui/namespace_manager/__init__.py +0 -10
- jettask/webui/namespace_manager/multi.py +0 -593
- jettask/webui/namespace_manager/unified.py +0 -193
- jettask/webui/run.py +0 -46
- jettask-0.2.20.dist-info/RECORD +0 -145
- {jettask-0.2.20.dist-info → jettask-0.2.24.dist-info}/WHEEL +0 -0
- {jettask-0.2.20.dist-info → jettask-0.2.24.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.20.dist-info → jettask-0.2.24.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.20.dist-info → jettask-0.2.24.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
"""
|
2
|
+
Task 模型
|
3
|
+
|
4
|
+
对应 tasks 表,用于存储任务消息(实际表结构)
|
5
|
+
"""
|
6
|
+
from sqlalchemy import Column, String, Integer, Text, TIMESTAMP, Index
|
7
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
8
|
+
from datetime import datetime
|
9
|
+
from typing import Optional, Dict, Any
|
10
|
+
|
11
|
+
from ..base import Base
|
12
|
+
|
13
|
+
|
14
|
+
class Task(Base):
|
15
|
+
"""
|
16
|
+
任务消息表
|
17
|
+
|
18
|
+
存储发送到队列的任务消息
|
19
|
+
"""
|
20
|
+
__tablename__ = 'tasks'
|
21
|
+
|
22
|
+
# 主键 - Redis Stream ID
|
23
|
+
stream_id = Column(Text, primary_key=True, comment='Redis Stream 事件ID')
|
24
|
+
|
25
|
+
# 队列和命名空间
|
26
|
+
queue = Column(Text, nullable=False, comment='队列名称')
|
27
|
+
namespace = Column(Text, nullable=False, comment='命名空间')
|
28
|
+
|
29
|
+
# 定时任务关联
|
30
|
+
scheduled_task_id = Column(Text, nullable=True, comment='关联的定时任务ID')
|
31
|
+
|
32
|
+
# 消息数据
|
33
|
+
payload = Column(JSONB, nullable=False, comment='消息载荷(任务数据)')
|
34
|
+
|
35
|
+
# 优先级
|
36
|
+
priority = Column(Integer, nullable=True, comment='优先级(数字越小优先级越高)')
|
37
|
+
|
38
|
+
# 时间戳
|
39
|
+
created_at = Column(
|
40
|
+
TIMESTAMP(timezone=True),
|
41
|
+
nullable=False,
|
42
|
+
default=datetime.utcnow,
|
43
|
+
comment='创建时间'
|
44
|
+
)
|
45
|
+
|
46
|
+
# 来源
|
47
|
+
source = Column(Text, nullable=False, comment='消息来源(如 scheduler, manual, api)')
|
48
|
+
|
49
|
+
# 元数据
|
50
|
+
task_metadata = Column('metadata', JSONB, nullable=True, comment='任务元数据')
|
51
|
+
|
52
|
+
# 索引
|
53
|
+
__table_args__ = (
|
54
|
+
Index('idx_tasks_queue', 'queue'),
|
55
|
+
Index('idx_tasks_namespace', 'namespace'),
|
56
|
+
Index('idx_tasks_queue_namespace', 'queue', 'namespace'),
|
57
|
+
Index('idx_tasks_scheduled_task_id', 'scheduled_task_id'),
|
58
|
+
Index('idx_tasks_created_at', 'created_at'),
|
59
|
+
Index('idx_tasks_source', 'source'),
|
60
|
+
)
|
61
|
+
|
62
|
+
def to_dict(self) -> Dict[str, Any]:
|
63
|
+
"""转换为字典"""
|
64
|
+
return {
|
65
|
+
'stream_id': self.stream_id,
|
66
|
+
'queue': self.queue,
|
67
|
+
'namespace': self.namespace,
|
68
|
+
'scheduled_task_id': self.scheduled_task_id,
|
69
|
+
'payload': self.payload,
|
70
|
+
'priority': self.priority,
|
71
|
+
'created_at': self.created_at.isoformat() if self.created_at else None,
|
72
|
+
'source': self.source,
|
73
|
+
'metadata': self.task_metadata,
|
74
|
+
}
|
75
|
+
|
76
|
+
def __repr__(self) -> str:
|
77
|
+
return f"<Task(stream_id='{self.stream_id}', queue='{self.queue}', namespace='{self.namespace}')>"
|
@@ -0,0 +1,85 @@
|
|
1
|
+
"""
|
2
|
+
TaskRun 模型
|
3
|
+
|
4
|
+
对应 task_runs 表,用于存储任务执行记录
|
5
|
+
"""
|
6
|
+
from sqlalchemy import Column, String, Integer, Text, TIMESTAMP, Float, Index, ForeignKey
|
7
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
8
|
+
from datetime import datetime
|
9
|
+
from typing import Optional, Dict, Any
|
10
|
+
|
11
|
+
from ..base import Base
|
12
|
+
|
13
|
+
|
14
|
+
class TaskRun(Base):
|
15
|
+
"""
|
16
|
+
任务执行记录表
|
17
|
+
|
18
|
+
存储每次任务执行的状态、结果和执行时间等信息
|
19
|
+
"""
|
20
|
+
__tablename__ = 'task_runs'
|
21
|
+
|
22
|
+
# 主键 - Redis Stream ID
|
23
|
+
stream_id = Column(Text, primary_key=True, comment='Redis Stream 事件ID,关联到 tasks 表')
|
24
|
+
|
25
|
+
# 执行状态
|
26
|
+
status = Column(String(50), nullable=True, comment='任务状态 (pending/running/success/failed/retrying)')
|
27
|
+
|
28
|
+
# 执行结果
|
29
|
+
result = Column(JSONB, nullable=True, comment='任务执行结果')
|
30
|
+
error = Column(Text, nullable=True, comment='错误信息(如果失败)')
|
31
|
+
|
32
|
+
# 执行时间
|
33
|
+
started_at = Column(Float, nullable=True, comment='开始执行时间(Unix 时间戳)')
|
34
|
+
completed_at = Column(Float, nullable=True, comment='完成时间(Unix 时间戳)')
|
35
|
+
|
36
|
+
# 重试次数
|
37
|
+
retries = Column(Integer, nullable=True, default=0, comment='重试次数')
|
38
|
+
|
39
|
+
# 执行时长(秒)
|
40
|
+
duration = Column(Float, nullable=True, comment='执行时长(秒)')
|
41
|
+
|
42
|
+
# 消费者信息
|
43
|
+
consumer = Column(Text, nullable=True, comment='执行该任务的消费者ID')
|
44
|
+
|
45
|
+
# 记录创建和更新时间
|
46
|
+
created_at = Column(
|
47
|
+
TIMESTAMP(timezone=True),
|
48
|
+
nullable=False,
|
49
|
+
default=datetime.utcnow,
|
50
|
+
comment='记录创建时间'
|
51
|
+
)
|
52
|
+
updated_at = Column(
|
53
|
+
TIMESTAMP(timezone=True),
|
54
|
+
nullable=False,
|
55
|
+
default=datetime.utcnow,
|
56
|
+
onupdate=datetime.utcnow,
|
57
|
+
comment='记录更新时间'
|
58
|
+
)
|
59
|
+
|
60
|
+
# 索引
|
61
|
+
__table_args__ = (
|
62
|
+
Index('idx_task_runs_status', 'status'),
|
63
|
+
Index('idx_task_runs_started_at', 'started_at'),
|
64
|
+
Index('idx_task_runs_completed_at', 'completed_at'),
|
65
|
+
Index('idx_task_runs_created_at', 'created_at'),
|
66
|
+
)
|
67
|
+
|
68
|
+
def to_dict(self) -> Dict[str, Any]:
|
69
|
+
"""转换为字典"""
|
70
|
+
return {
|
71
|
+
'stream_id': self.stream_id,
|
72
|
+
'status': self.status,
|
73
|
+
'result': self.result,
|
74
|
+
'error': self.error,
|
75
|
+
'started_at': self.started_at,
|
76
|
+
'completed_at': self.completed_at,
|
77
|
+
'retries': self.retries,
|
78
|
+
'duration': self.duration,
|
79
|
+
'consumer': self.consumer,
|
80
|
+
'created_at': self.created_at.isoformat() if self.created_at else None,
|
81
|
+
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
82
|
+
}
|
83
|
+
|
84
|
+
def __repr__(self) -> str:
|
85
|
+
return f"<TaskRun(stream_id='{self.stream_id}', status='{self.status}')>"
|
jettask/executor/__init__.py
CHANGED
@@ -13,18 +13,6 @@ from .core import ExecutorCore
|
|
13
13
|
from .orchestrator import ProcessConfig, ProcessOrchestrator
|
14
14
|
from .task_executor import TaskExecutor
|
15
15
|
|
16
|
-
# 保留 UnifiedExecutor 作为兼容(已废弃)
|
17
|
-
try:
|
18
|
-
from .executor import UnifiedExecutor
|
19
|
-
except ImportError:
|
20
|
-
# 如果导入失败,提供一个废弃提示
|
21
|
-
class UnifiedExecutor:
|
22
|
-
def __init__(self, *args, **kwargs):
|
23
|
-
raise DeprecationWarning(
|
24
|
-
"UnifiedExecutor is deprecated. "
|
25
|
-
"Use TaskExecutor for single task execution, "
|
26
|
-
"or ProcessOrchestrator for multi-process management."
|
27
|
-
)
|
28
16
|
|
29
17
|
__all__ = [
|
30
18
|
# 新的推荐类
|
@@ -32,7 +20,4 @@ __all__ = [
|
|
32
20
|
'ProcessOrchestrator',
|
33
21
|
'ProcessConfig',
|
34
22
|
'ExecutorCore',
|
35
|
-
|
36
|
-
# 废弃但保留兼容性
|
37
|
-
'UnifiedExecutor',
|
38
23
|
]
|
jettask/executor/core.py
CHANGED
@@ -14,7 +14,7 @@ import logging
|
|
14
14
|
import time
|
15
15
|
import os
|
16
16
|
from enum import Enum
|
17
|
-
from typing import Dict, Optional
|
17
|
+
from typing import Dict, Optional, TYPE_CHECKING
|
18
18
|
from collections import defaultdict, deque
|
19
19
|
|
20
20
|
from ..utils.traceback_filter import filter_framework_traceback
|
@@ -23,6 +23,9 @@ from ..utils.serializer import dumps_str
|
|
23
23
|
from ..exceptions import RetryableError
|
24
24
|
from ..core.enums import TaskStatus
|
25
25
|
from ..utils.rate_limit.limiter import RateLimiterManager, ConcurrencyRateLimiter
|
26
|
+
from ..config.lua_scripts import LUA_SCRIPT_BATCH_SEND_EVENT
|
27
|
+
if TYPE_CHECKING:
|
28
|
+
from ..core.app import Jettask
|
26
29
|
|
27
30
|
logger = logging.getLogger('app')
|
28
31
|
|
@@ -61,7 +64,7 @@ class ExecutorCore:
|
|
61
64
|
4. 统计收集
|
62
65
|
"""
|
63
66
|
|
64
|
-
def __init__(self, app, task_name: str, concurrency: int = 100):
|
67
|
+
def __init__(self, app: "Jettask", task_name: str, concurrency: int = 100):
|
65
68
|
"""
|
66
69
|
初始化执行器核心
|
67
70
|
|
@@ -218,39 +221,37 @@ class ExecutorCore:
|
|
218
221
|
operations_count += 1
|
219
222
|
|
220
223
|
self.pending_acks.clear()
|
221
|
-
|
222
224
|
# 2. 处理任务信息更新
|
223
225
|
task_change_events = []
|
224
226
|
if self.task_info_updates:
|
225
227
|
for event_key, updates in self.task_info_updates.items():
|
226
|
-
|
228
|
+
if event_key.endswith('consumer._handle_status_update') or \
|
229
|
+
event_key.endswith('consumer._handle_persist_task'):
|
230
|
+
continue # 跳过无效的event_key
|
231
|
+
key = f"{self.prefix}:TASK:{event_key}"
|
232
|
+
key_bytes = key.encode()
|
227
233
|
if updates:
|
228
234
|
encoded_updates = {k.encode(): v.encode() if isinstance(v, str) else v
|
229
235
|
for k, v in updates.items()}
|
230
|
-
pipeline.hset(
|
231
|
-
pipeline.expire(
|
236
|
+
pipeline.hset(key_bytes, mapping=encoded_updates)
|
237
|
+
pipeline.expire(key_bytes, 3600)
|
232
238
|
operations_count += 2
|
239
|
+
task_change_events.append(key)
|
233
240
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
# 发送变更事件
|
238
|
-
change_stream_key = f"{self.prefix}:TASK_CHANGES".encode()
|
239
|
-
for task_id in task_change_events:
|
240
|
-
change_data = {b'id': task_id.encode() if isinstance(task_id, str) else task_id}
|
241
|
-
pipeline.xadd(change_stream_key, change_data, maxlen=1000000)
|
242
|
-
operations_count += 1
|
241
|
+
# 发送变更事件(使用 Lua 脚本确保 offset 更新和 REGISTRY 注册)
|
242
|
+
if task_change_events:
|
243
|
+
self._send_task_changes_with_offset(task_change_events, pipeline)
|
243
244
|
|
244
245
|
self.task_info_updates.clear()
|
245
246
|
|
246
247
|
# 3. 处理统计信息
|
247
248
|
if hasattr(self, 'stats_updates') and self.stats_updates:
|
248
|
-
for stat_op in self.stats_updates:
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
249
|
+
# for stat_op in self.stats_updates:
|
250
|
+
# if 'queue' in stat_op and 'field' in stat_op:
|
251
|
+
# stats_key = f"{self.prefix}:STATS:{stat_op['queue']}".encode()
|
252
|
+
# field = stat_op['field'].encode() if isinstance(stat_op['field'], str) else stat_op['field']
|
253
|
+
# pipeline.hincrby(stats_key, field, stat_op.get('value', 1))
|
254
|
+
# operations_count += 1
|
254
255
|
self.stats_updates.clear()
|
255
256
|
|
256
257
|
# 执行pipeline
|
@@ -427,10 +428,20 @@ class ExecutorCore:
|
|
427
428
|
if current_retry > 0:
|
428
429
|
logger.debug(f"Retry attempt {current_retry}/{max_retries} for task {event_id}")
|
429
430
|
|
431
|
+
# 不过滤 __ 开头的参数,因为它们需要在 _inject_dependencies 中被提取
|
432
|
+
# (如 __scheduled_task_id, __queue 等)
|
433
|
+
# 只过滤单下划线开头的参数
|
430
434
|
clean_kwargs = {k: v for k, v in kwargs_inner.items()
|
431
|
-
if not k.startswith('_')
|
432
|
-
|
433
|
-
|
435
|
+
if not k.startswith('_') or k.startswith('__')}
|
436
|
+
|
437
|
+
print(f'{queue=} {args=} {kwargs_inner=}')
|
438
|
+
task_result = task(
|
439
|
+
event_id,
|
440
|
+
event_data.get('trigger_time'),
|
441
|
+
queue,
|
442
|
+
*args,
|
443
|
+
**clean_kwargs
|
444
|
+
)
|
434
445
|
if asyncio.iscoroutine(task_result):
|
435
446
|
ret = await task_result
|
436
447
|
else:
|
@@ -445,13 +456,14 @@ class ExecutorCore:
|
|
445
456
|
if asyncio.iscoroutine(result):
|
446
457
|
await result
|
447
458
|
|
448
|
-
# 任务成功,ACK
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
459
|
+
# 任务成功,ACK消息(检查auto_ack配置)
|
460
|
+
task_config = self.app.get_task_config(task_name)
|
461
|
+
auto_ack = task_config.get('auto_ack', True) if task_config else True
|
462
|
+
|
463
|
+
if auto_ack:
|
464
|
+
offset = self._extract_offset(event_data)
|
465
|
+
await self._quick_ack(queue, event_id, group_name, offset)
|
466
|
+
|
455
467
|
break
|
456
468
|
|
457
469
|
except SystemExit:
|
@@ -599,6 +611,39 @@ class ExecutorCore:
|
|
599
611
|
pass
|
600
612
|
return None
|
601
613
|
|
614
|
+
def _send_task_changes_with_offset(self, task_ids: list, pipeline):
|
615
|
+
"""发送 TASK_CHANGES 消息并自动更新 offset
|
616
|
+
|
617
|
+
使用 Lua 脚本确保:
|
618
|
+
1. 自动递增 offset
|
619
|
+
2. 注册队列到 REGISTRY:QUEUES
|
620
|
+
3. 消息中包含 offset 字段
|
621
|
+
|
622
|
+
Args:
|
623
|
+
task_ids: 任务 ID 列表
|
624
|
+
pipeline: Redis pipeline
|
625
|
+
"""
|
626
|
+
from jettask.utils.serializer import dumps_str
|
627
|
+
|
628
|
+
# 准备Lua脚本参数
|
629
|
+
stream_key = f"{self.prefix}:QUEUE:TASK_CHANGES".encode()
|
630
|
+
lua_args = [self.prefix.encode() if isinstance(self.prefix, str) else self.prefix]
|
631
|
+
|
632
|
+
# 为每个 task_id 构建消息
|
633
|
+
for task_id in task_ids:
|
634
|
+
message_data = {'kwargs': {'task_id': task_id}}
|
635
|
+
data = dumps_str(message_data)
|
636
|
+
lua_args.append(data)
|
637
|
+
|
638
|
+
# 使用 pipeline 执行 Lua 脚本
|
639
|
+
pipeline.eval(
|
640
|
+
LUA_SCRIPT_BATCH_SEND_EVENT,
|
641
|
+
1, # 1个KEY
|
642
|
+
stream_key, # KEY[1]: stream key
|
643
|
+
*lua_args # ARGV: prefix, data1, data2, ...
|
644
|
+
)
|
645
|
+
logger.debug(f"已添加 {len(task_ids)} 条 TASK_CHANGES 消息到 pipeline")
|
646
|
+
|
602
647
|
async def cleanup(self):
|
603
648
|
"""清理资源"""
|
604
649
|
logger.debug("ExecutorCore cleaning up...")
|
@@ -16,116 +16,11 @@ import multiprocessing
|
|
16
16
|
from typing import List, Dict
|
17
17
|
import time
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
import redis
|
24
|
-
async def execute_single_command(client: redis.Redis, index: int):
|
25
|
-
"""
|
26
|
-
执行单条 Redis 命令
|
27
|
-
|
28
|
-
Args:
|
29
|
-
client: Redis 客户端
|
30
|
-
index: 命令索引
|
31
|
-
"""
|
32
|
-
await client.set(f"test_key_{index}", f"value_{index}")
|
33
|
-
|
34
|
-
|
35
|
-
async def execute_commands(client: redis.Redis, num_commands: int = 10000, concurrency: int = 100):
|
36
|
-
"""
|
37
|
-
并发执行大量 Redis 命令
|
38
|
-
|
39
|
-
Args:
|
40
|
-
client: Redis 客户端
|
41
|
-
num_commands: 命令数量
|
42
|
-
concurrency: 并发数(同时执行的任务数)
|
43
|
-
"""
|
44
|
-
logger.info(f"开始并发执行 {num_commands} 条 Redis 命令(并发数: {concurrency})...")
|
45
|
-
|
46
|
-
start_time = time.time()
|
47
|
-
|
48
|
-
# 创建信号量控制并发数
|
49
|
-
semaphore = asyncio.Semaphore(concurrency)
|
50
|
-
|
51
|
-
async def execute_with_semaphore(index: int):
|
52
|
-
"""使用信号量控制并发"""
|
53
|
-
# async with semaphore:
|
54
|
-
await execute_single_command(client, index)
|
55
|
-
|
56
|
-
# 每 1000 条命令打印一次进度
|
57
|
-
if (index + 1) % 1000 == 0:
|
58
|
-
logger.info(f"已完成 {index + 1} 条命令")
|
59
|
-
|
60
|
-
# 创建所有任务
|
61
|
-
tasks = [
|
62
|
-
asyncio.create_task(execute_with_semaphore(i))
|
63
|
-
for i in range(num_commands)
|
64
|
-
]
|
65
|
-
|
66
|
-
# 等待所有任务完成
|
67
|
-
await asyncio.gather(*tasks)
|
68
|
-
|
69
|
-
elapsed = time.time() - start_time
|
70
|
-
logger.info(f"✅ 完成 {num_commands} 条命令,耗时: {elapsed:.2f}s,QPS: {num_commands/elapsed:.0f}")
|
71
|
-
await asyncio.sleep(999999999999999999)
|
72
|
-
return elapsed
|
73
|
-
|
74
|
-
|
75
19
|
# 不要在模块级别创建 logger,避免在 fork 时触发 logging 全局锁竞争
|
76
20
|
# logger 将在 subprocess_main 中创建
|
77
21
|
logger = None
|
78
22
|
|
79
23
|
|
80
|
-
def _reset_locks_after_fork():
|
81
|
-
"""
|
82
|
-
在 fork 后立即重置所有 logging 相关的锁
|
83
|
-
|
84
|
-
关键:不能调用任何 logging API(如 getLogger),因为这些 API 本身需要获取锁。
|
85
|
-
必须直接访问 logging 模块的内部数据结构。
|
86
|
-
"""
|
87
|
-
import threading
|
88
|
-
import logging
|
89
|
-
|
90
|
-
# 1. 暴力替换 logging 模块的全局锁(不通过 API)
|
91
|
-
logging._lock = threading.RLock()
|
92
|
-
|
93
|
-
# 2. 直接访问 Logger.manager 的内部字典,不调用 getLogger()
|
94
|
-
# 这样避免触发锁的获取
|
95
|
-
if hasattr(logging.Logger, 'manager'):
|
96
|
-
manager = logging.Logger.manager
|
97
|
-
|
98
|
-
# 2.1 重置 manager 的锁
|
99
|
-
if hasattr(manager, 'lock'):
|
100
|
-
manager.lock = threading.RLock()
|
101
|
-
|
102
|
-
# 2.2 重置根 logger 的锁(直接访问 root 属性)
|
103
|
-
if hasattr(manager, 'root'):
|
104
|
-
root = manager.root
|
105
|
-
if hasattr(root, '_lock'):
|
106
|
-
root._lock = threading.RLock()
|
107
|
-
|
108
|
-
# 重置根 logger 的所有 handlers 的锁
|
109
|
-
if hasattr(root, 'handlers'):
|
110
|
-
for handler in list(root.handlers):
|
111
|
-
if hasattr(handler, 'lock'):
|
112
|
-
handler.lock = threading.RLock()
|
113
|
-
|
114
|
-
# 2.3 重置所有子 logger 的锁(直接访问字典,不调用 getLogger)
|
115
|
-
if hasattr(manager, 'loggerDict'):
|
116
|
-
for logger_obj in list(manager.loggerDict.values()):
|
117
|
-
# loggerDict 中的值可能是 PlaceHolder 或 Logger
|
118
|
-
if hasattr(logger_obj, '_lock'):
|
119
|
-
logger_obj._lock = threading.RLock()
|
120
|
-
|
121
|
-
# 重置 handlers 的锁
|
122
|
-
if hasattr(logger_obj, 'handlers'):
|
123
|
-
for handler in list(logger_obj.handlers):
|
124
|
-
if hasattr(handler, 'lock'):
|
125
|
-
handler.lock = threading.RLock()
|
126
|
-
|
127
|
-
|
128
|
-
|
129
24
|
class SubprocessInitializer:
|
130
25
|
"""子进程初始化器 - 负责清理和准备环境"""
|
131
26
|
|
@@ -160,7 +55,7 @@ class SubprocessInitializer:
|
|
160
55
|
|
161
56
|
# 3. 清空Redis连接池和客户端缓存
|
162
57
|
# 这非常重要!防止子进程复用父进程的连接
|
163
|
-
from jettask.
|
58
|
+
from jettask.db.connector import clear_all_cache
|
164
59
|
clear_all_cache()
|
165
60
|
|
166
61
|
# 4. 强制垃圾回收
|
@@ -291,6 +186,28 @@ class MinimalApp:
|
|
291
186
|
"""根据任务名称获取任务对象"""
|
292
187
|
return self._tasks.get(task_name)
|
293
188
|
|
189
|
+
def get_task_config(self, task_name: str) -> dict:
|
190
|
+
"""
|
191
|
+
获取任务配置
|
192
|
+
|
193
|
+
Args:
|
194
|
+
task_name: 任务名称
|
195
|
+
|
196
|
+
Returns:
|
197
|
+
任务配置字典,如果任务不存在则返回None
|
198
|
+
"""
|
199
|
+
task = self.get_task_by_name(task_name)
|
200
|
+
if not task:
|
201
|
+
return None
|
202
|
+
|
203
|
+
return {
|
204
|
+
'auto_ack': getattr(task, 'auto_ack', True),
|
205
|
+
'queue': getattr(task, 'queue', None),
|
206
|
+
'timeout': getattr(task, 'timeout', None),
|
207
|
+
'max_retries': getattr(task, 'max_retries', 0),
|
208
|
+
'retry_delay': getattr(task, 'retry_delay', None),
|
209
|
+
}
|
210
|
+
|
294
211
|
def cleanup(self):
|
295
212
|
"""清理资源"""
|
296
213
|
pass
|
@@ -345,12 +262,12 @@ class SubprocessRunner:
|
|
345
262
|
if self.event_pool:
|
346
263
|
self.event_pool._stop_reading = True
|
347
264
|
|
348
|
-
|
349
|
-
|
265
|
+
signal.signal(signal.SIGINT, signal_handler)
|
266
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
350
267
|
|
351
268
|
def create_redis_connections(self):
|
352
269
|
"""创建独立的Redis连接(使用全局客户端实例)"""
|
353
|
-
from jettask.
|
270
|
+
from jettask.db.connector import get_sync_redis_client, get_async_redis_client
|
354
271
|
|
355
272
|
# logger.info(f"Process #{self.process_id}: Creating Redis connections")
|
356
273
|
|
@@ -394,16 +311,13 @@ class SubprocessRunner:
|
|
394
311
|
self.event_pool = EventPool(
|
395
312
|
self.redis_client,
|
396
313
|
self.async_redis_client,
|
314
|
+
queues=self.queues, # ✅ 传递 queues 参数以支持通配符模式
|
397
315
|
redis_url=self.redis_url,
|
398
316
|
consumer_strategy=self.consumer_strategy,
|
399
317
|
consumer_config=consumer_config,
|
400
318
|
redis_prefix=self.redis_prefix,
|
401
319
|
app=self.minimal_app
|
402
320
|
)
|
403
|
-
|
404
|
-
|
405
|
-
# logger.info('准备进入测试流程')
|
406
|
-
# await execute_commands(self.event_pool.async_redis_client, num_commands=100000)
|
407
321
|
|
408
322
|
# 将 EventPool 设置到 MinimalApp
|
409
323
|
self.minimal_app.ep = self.event_pool
|
@@ -418,7 +332,8 @@ class SubprocessRunner:
|
|
418
332
|
)
|
419
333
|
|
420
334
|
# 初始化路由
|
421
|
-
|
335
|
+
# ✅ 不再需要这行,因为 EventPool.__init__ 已经正确处理了 queues(包括通配符)
|
336
|
+
# self.event_pool.queues = self.queues
|
422
337
|
self.event_pool.init_routing()
|
423
338
|
|
424
339
|
# 收集任务(按队列分组)
|