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.
Files changed (110) hide show
  1. jettask/__init__.py +4 -0
  2. jettask/cli.py +12 -8
  3. jettask/config/lua_scripts.py +37 -0
  4. jettask/config/nacos_config.py +1 -1
  5. jettask/core/app.py +313 -340
  6. jettask/core/container.py +4 -4
  7. jettask/{persistence → core}/namespace.py +93 -27
  8. jettask/core/task.py +16 -9
  9. jettask/core/unified_manager_base.py +136 -26
  10. jettask/db/__init__.py +67 -0
  11. jettask/db/base.py +137 -0
  12. jettask/{utils/db_connector.py → db/connector.py} +130 -26
  13. jettask/db/models/__init__.py +16 -0
  14. jettask/db/models/scheduled_task.py +196 -0
  15. jettask/db/models/task.py +77 -0
  16. jettask/db/models/task_run.py +85 -0
  17. jettask/executor/__init__.py +0 -15
  18. jettask/executor/core.py +76 -31
  19. jettask/executor/process_entry.py +29 -114
  20. jettask/executor/task_executor.py +4 -0
  21. jettask/messaging/event_pool.py +928 -685
  22. jettask/messaging/scanner.py +30 -0
  23. jettask/persistence/__init__.py +28 -103
  24. jettask/persistence/buffer.py +170 -0
  25. jettask/persistence/consumer.py +330 -249
  26. jettask/persistence/manager.py +304 -0
  27. jettask/persistence/persistence.py +391 -0
  28. jettask/scheduler/__init__.py +15 -3
  29. jettask/scheduler/{task_crud.py → database.py} +61 -57
  30. jettask/scheduler/loader.py +2 -2
  31. jettask/scheduler/{scheduler_coordinator.py → manager.py} +23 -6
  32. jettask/scheduler/models.py +14 -10
  33. jettask/scheduler/schedule.py +166 -0
  34. jettask/scheduler/scheduler.py +12 -11
  35. jettask/schemas/__init__.py +50 -1
  36. jettask/schemas/backlog.py +43 -6
  37. jettask/schemas/namespace.py +70 -19
  38. jettask/schemas/queue.py +19 -3
  39. jettask/schemas/responses.py +493 -0
  40. jettask/task/__init__.py +0 -2
  41. jettask/task/router.py +3 -0
  42. jettask/test_connection_monitor.py +1 -1
  43. jettask/utils/__init__.py +7 -5
  44. jettask/utils/db_init.py +8 -4
  45. jettask/utils/namespace_dep.py +167 -0
  46. jettask/utils/queue_matcher.py +186 -0
  47. jettask/utils/rate_limit/concurrency_limiter.py +7 -1
  48. jettask/utils/stream_backlog.py +1 -1
  49. jettask/webui/__init__.py +0 -1
  50. jettask/webui/api/__init__.py +4 -4
  51. jettask/webui/api/alerts.py +806 -71
  52. jettask/webui/api/example_refactored.py +400 -0
  53. jettask/webui/api/namespaces.py +390 -45
  54. jettask/webui/api/overview.py +300 -54
  55. jettask/webui/api/queues.py +971 -267
  56. jettask/webui/api/scheduled.py +1249 -56
  57. jettask/webui/api/settings.py +129 -7
  58. jettask/webui/api/workers.py +442 -0
  59. jettask/webui/app.py +46 -2329
  60. jettask/webui/middleware/__init__.py +6 -0
  61. jettask/webui/middleware/namespace_middleware.py +135 -0
  62. jettask/webui/services/__init__.py +146 -0
  63. jettask/webui/services/heartbeat_service.py +251 -0
  64. jettask/webui/services/overview_service.py +60 -51
  65. jettask/webui/services/queue_monitor_service.py +426 -0
  66. jettask/webui/services/redis_monitor_service.py +87 -0
  67. jettask/webui/services/settings_service.py +174 -111
  68. jettask/webui/services/task_monitor_service.py +222 -0
  69. jettask/webui/services/timeline_pg_service.py +452 -0
  70. jettask/webui/services/timeline_service.py +189 -0
  71. jettask/webui/services/worker_monitor_service.py +467 -0
  72. jettask/webui/utils/__init__.py +11 -0
  73. jettask/webui/utils/time_utils.py +122 -0
  74. jettask/worker/lifecycle.py +8 -2
  75. {jettask-0.2.20.dist-info → jettask-0.2.24.dist-info}/METADATA +1 -1
  76. jettask-0.2.24.dist-info/RECORD +142 -0
  77. jettask/executor/executor.py +0 -338
  78. jettask/persistence/backlog_monitor.py +0 -567
  79. jettask/persistence/base.py +0 -2334
  80. jettask/persistence/db_manager.py +0 -516
  81. jettask/persistence/maintenance.py +0 -81
  82. jettask/persistence/message_consumer.py +0 -259
  83. jettask/persistence/models.py +0 -49
  84. jettask/persistence/offline_recovery.py +0 -196
  85. jettask/persistence/queue_discovery.py +0 -215
  86. jettask/persistence/task_persistence.py +0 -218
  87. jettask/persistence/task_updater.py +0 -583
  88. jettask/scheduler/add_execution_count.sql +0 -11
  89. jettask/scheduler/add_priority_field.sql +0 -26
  90. jettask/scheduler/add_scheduler_id.sql +0 -25
  91. jettask/scheduler/add_scheduler_id_index.sql +0 -10
  92. jettask/scheduler/make_scheduler_id_required.sql +0 -28
  93. jettask/scheduler/migrate_interval_seconds.sql +0 -9
  94. jettask/scheduler/performance_optimization.sql +0 -45
  95. jettask/scheduler/run_scheduler.py +0 -186
  96. jettask/scheduler/schema.sql +0 -84
  97. jettask/task/task_executor.py +0 -318
  98. jettask/webui/api/analytics.py +0 -323
  99. jettask/webui/config.py +0 -90
  100. jettask/webui/models/__init__.py +0 -3
  101. jettask/webui/models/namespace.py +0 -63
  102. jettask/webui/namespace_manager/__init__.py +0 -10
  103. jettask/webui/namespace_manager/multi.py +0 -593
  104. jettask/webui/namespace_manager/unified.py +0 -193
  105. jettask/webui/run.py +0 -46
  106. jettask-0.2.20.dist-info/RECORD +0 -145
  107. {jettask-0.2.20.dist-info → jettask-0.2.24.dist-info}/WHEEL +0 -0
  108. {jettask-0.2.20.dist-info → jettask-0.2.24.dist-info}/entry_points.txt +0 -0
  109. {jettask-0.2.20.dist-info → jettask-0.2.24.dist-info}/licenses/LICENSE +0 -0
  110. {jettask-0.2.20.dist-info → jettask-0.2.24.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,122 @@
1
+ """
2
+ 时间处理工具函数
3
+ """
4
+ import json
5
+ from datetime import datetime, timezone
6
+ from typing import Dict, Any, List
7
+
8
+
9
+ def parse_iso_datetime(time_str: str) -> datetime:
10
+ """
11
+ 解析 ISO 格式的时间字符串,确保返回 UTC 时间
12
+
13
+ Args:
14
+ time_str: ISO 格式的时间字符串
15
+
16
+ Returns:
17
+ UTC 时间的 datetime 对象
18
+ """
19
+ if time_str.endswith('Z'):
20
+ # Z 表示 UTC 时间
21
+ dt = datetime.fromisoformat(time_str.replace('Z', '+00:00'))
22
+ else:
23
+ dt = datetime.fromisoformat(time_str)
24
+
25
+ # 如果没有时区信息,假定为 UTC
26
+ if dt.tzinfo is None:
27
+ dt = dt.replace(tzinfo=timezone.utc)
28
+ # 如果有时区信息,转换为 UTC
29
+ elif dt.tzinfo != timezone.utc:
30
+ dt = dt.astimezone(timezone.utc)
31
+
32
+ return dt
33
+
34
+
35
+ def format_task_timestamps(task: Dict[str, Any], fields: List[str] = None) -> Dict[str, Any]:
36
+ """
37
+ 将任务对象中的时间字段转换为 ISO 格式字符串
38
+
39
+ Args:
40
+ task: 任务字典
41
+ fields: 需要转换的字段列表,默认为 ['created_at', 'started_at', 'completed_at']
42
+
43
+ Returns:
44
+ 转换后的任务字典
45
+ """
46
+ if fields is None:
47
+ fields = ['created_at', 'started_at', 'completed_at']
48
+
49
+ for field in fields:
50
+ if task.get(field):
51
+ # PostgreSQL 的 TIMESTAMP WITH TIME ZONE 会返回 aware datetime
52
+ if task[field].tzinfo is None:
53
+ # 如果没有时区信息,假定为 UTC
54
+ task[field] = task[field].replace(tzinfo=timezone.utc)
55
+ task[field] = task[field].isoformat()
56
+
57
+ return task
58
+
59
+
60
+ def parse_task_json_fields(task: Dict[str, Any], fields: List[str] = None) -> Dict[str, Any]:
61
+ """
62
+ 解析任务对象中的 JSON 字段
63
+
64
+ Args:
65
+ task: 任务字典
66
+ fields: 需要解析的字段列表,默认为 ['task_data', 'result', 'metadata']
67
+
68
+ Returns:
69
+ 解析后的任务字典
70
+ """
71
+ if fields is None:
72
+ fields = ['task_data', 'result', 'metadata']
73
+
74
+ for field in fields:
75
+ if task.get(field) and isinstance(task[field], str):
76
+ try:
77
+ task[field] = json.loads(task[field])
78
+ except:
79
+ pass
80
+
81
+ return task
82
+
83
+
84
+ def task_obj_to_dict(task_obj) -> Dict[str, Any]:
85
+ """
86
+ 将 Task ORM 对象转换为字典
87
+
88
+ Args:
89
+ task_obj: Task ORM 对象
90
+
91
+ Returns:
92
+ 任务字典
93
+ """
94
+ task = {
95
+ 'id': task_obj.id,
96
+ 'queue_name': task_obj.queue_name,
97
+ 'task_name': task_obj.task_name,
98
+ 'task_data': task_obj.task_data,
99
+ 'priority': task_obj.priority,
100
+ 'retry_count': task_obj.retry_count,
101
+ 'max_retry': task_obj.max_retry,
102
+ 'status': task_obj.status,
103
+ 'result': task_obj.result,
104
+ 'error_message': task_obj.error_message,
105
+ 'created_at': task_obj.created_at,
106
+ 'started_at': task_obj.started_at,
107
+ 'completed_at': task_obj.completed_at,
108
+ 'worker_id': task_obj.worker_id,
109
+ 'execution_time': task_obj.execution_time,
110
+ 'duration': task_obj.duration,
111
+ 'metadata': task_obj.task_metadata,
112
+ 'next_sync_time': task_obj.next_sync_time,
113
+ 'sync_check_count': task_obj.sync_check_count
114
+ }
115
+
116
+ # 转换时间戳为 ISO 格式
117
+ task = format_task_timestamps(task)
118
+
119
+ # 解析 JSON 字段
120
+ task = parse_task_json_fields(task)
121
+
122
+ return task
@@ -366,6 +366,12 @@ class WorkerStateManager:
366
366
  pubsub = self.redis.pubsub()
367
367
  await pubsub.subscribe(self.worker_state_channel)
368
368
 
369
+ # 标记PubSub连接,防止被空闲连接清理
370
+ if hasattr(pubsub, 'connection') and pubsub.connection:
371
+ pubsub.connection._is_pubsub_connection = True
372
+ socket_timeout = pubsub.connection.socket_timeout if hasattr(pubsub.connection, 'socket_timeout') else 'N/A'
373
+ logger.info(f"Marked PubSub connection {id(pubsub.connection)} to prevent cleanup, socket_timeout={socket_timeout}")
374
+
369
375
  logger.debug(f"Created and subscribed to Redis Pub/Sub channel: {self.worker_state_channel}")
370
376
  return pubsub
371
377
 
@@ -739,7 +745,7 @@ class HeartbeatThreadManager:
739
745
  heartbeat_timeout=15.0):
740
746
  """初始化心跳线程管理器"""
741
747
  if redis_url is not None:
742
- from jettask.utils.db_connector import get_sync_redis_client
748
+ from jettask.db.connector import get_sync_redis_client
743
749
  self.redis_client = get_sync_redis_client(redis_url, decode_responses=True)
744
750
  self.redis_url = redis_url
745
751
  self.consumer_id = consumer_id
@@ -992,7 +998,7 @@ class HeartbeatThreadManager:
992
998
  pass
993
999
  try:
994
1000
  if self.redis_url:
995
- from jettask.utils.db_connector import get_sync_redis_client
1001
+ from jettask.db.connector import get_sync_redis_client
996
1002
  self.redis_client = get_sync_redis_client(
997
1003
  redis_url=self.redis_url,
998
1004
  decode_responses=True,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jettask
3
- Version: 0.2.20
3
+ Version: 0.2.24
4
4
  Summary: A high-performance distributed task queue system with web monitoring
5
5
  Author-email: JetTask Team <support@jettask.io>
6
6
  License-Expression: MIT
@@ -0,0 +1,142 @@
1
+ jettask/__init__.py,sha256=4TsONE4CfUIRgLvZMDvCOoEZWCA2qx11ZONaaqmhsls,2012
2
+ jettask/cli.py,sha256=0pxM2Kvoa-hM2gE3q_nYXjfWJy_8pXXZSxIjKe3z8uw,38708
3
+ jettask/exceptions.py,sha256=aQTUpiF2ftiZwGORUHXatAKbDEzptUf6iP7ZOXBQ3tQ,1971
4
+ jettask/test_connection_monitor.py,sha256=aJmWDcC8XoJVEKfnQbixof9VysxFUMfHvIoI2TV1eS0,2189
5
+ jettask/config/__init__.py,sha256=gCfoHVfTWIRPbtY1-iE4ydf2lfOmEwTU3EoSJ7NbLIU,273
6
+ jettask/config/config.py,sha256=YdResg9uUD1rbmoumoIE-FkhMZ_skoRLgTlUjkLEEzQ,7111
7
+ jettask/config/constants.py,sha256=rw06U4WPSFk6f80blC0ebkqnPUS_eteKQesTSBXKn6A,7028
8
+ jettask/config/env_loader.py,sha256=P2uO63mAHfhu9Pqa_Hh0JKWmeFDptudSHgBI-fyr0sA,10558
9
+ jettask/config/lua_scripts.py,sha256=40nFzJNm6cZHfKZ7ovYRMVofS5bxWFGU-Dq0RS4PCdo,6070
10
+ jettask/config/nacos_config.py,sha256=PPmb-2gYTfgrdianjQdB9B4xYLAfRSihHEalccZXpo8,19689
11
+ jettask/config/performance.py,sha256=bOdLEskfB_6cRfS10IRgmtKEsJw_CaIZsPHbXxaHwbU,5686
12
+ jettask/config/task_center.py,sha256=dJ9UkWRR1kulHFhfSVXmT_bM-N7yEMJdp0JylUkrLZ4,2062
13
+ jettask/core/__init__.py,sha256=3qnUjllxsvy9gqJEKnJbQeJcSEmxdWUwwzumdJG5vKI,198
14
+ jettask/core/app.py,sha256=ycR5G9JBSMi941MyCnC0RG_3RyI4wpl1XQgq20F16NQ,103722
15
+ jettask/core/app_importer.py,sha256=04wRDCp5NFNiDkDhMOM7CdaHS_0b0cYylnYpXie8fwM,11018
16
+ jettask/core/container.py,sha256=Q4eGqkaw6C8jW3PyrmG5RrcCz3NA1ky2HelPx4Nn-xU,18108
17
+ jettask/core/context.py,sha256=XI4Q1s75NIWImcrHIkCLgGo_kpuJIk8MlBoNIJuEfF0,993
18
+ jettask/core/enums.py,sha256=ufXI5OcGZoOSbLEyAWtnkZVO-LcqNbIOygyjLLa6ce8,4632
19
+ jettask/core/message.py,sha256=WDOY8kvoV6at4MsIP01c3GJmIwHvRnUHimli6NKcw8w,4340
20
+ jettask/core/namespace.py,sha256=_EVMGWB4BrHf5Z6Zmdof3Uim9uX7-4IQ40HjLmE_WRM,28616
21
+ jettask/core/retry.py,sha256=lX56Z0fvKjEhVbYPHYdoN-W4XUPVplHlfIOupBylEeI,10711
22
+ jettask/core/task.py,sha256=Ada-wJLZLexrZtKLVqRDFSQOsvAnQpZnxPTDdoJnQq0,5712
23
+ jettask/core/unified_manager_base.py,sha256=JBBZUEolQHlHbeuNU3k8TwOBh7kxgs5z4yb8FbhbdKg,18987
24
+ jettask/db/__init__.py,sha256=FOYiPJs8B2ek-SgaszCTcBuzWaMKKTAwPTfqBHBDIJo,1388
25
+ jettask/db/base.py,sha256=vhrPErPKIkSsAEcF7l3FCZ4e4iNUjocwH0mkQ7MECQI,3769
26
+ jettask/db/connector.py,sha256=f3yfOhjadXDQEU9hBHIciV80tze3ugkaFn1uQgQz7To,63906
27
+ jettask/db/models/__init__.py,sha256=M9WlVfOOvovX5b_4aO0_NsEiQrxcV8zih2nITSbWiis,277
28
+ jettask/db/models/scheduled_task.py,sha256=Xxz3ayhyAdoFhCPEJUmDpNgp6pZdhhzCByp6PI9DiRc,7263
29
+ jettask/db/models/task.py,sha256=oliM4ZHM6bIANvlYGLjhivgmcGAvG_686--aRxCHljE,2441
30
+ jettask/db/models/task_run.py,sha256=SYIJtrAnnQMO6VvrK0_eD4z24gVHgRS6uoUwQwQDhLs,2826
31
+ jettask/executor/__init__.py,sha256=mdGkv9IsTxSJEfMg8b8mzw_RIGuPT05o2OxHxpKUWWQ,498
32
+ jettask/executor/core.py,sha256=Nf7tZ0ZxuSfKDqYzwQTIlFtdsNjWTbxj6-UHVVdTAVs,27119
33
+ jettask/executor/orchestrator.py,sha256=DS5dYnYnp7sISLb5hLBcPNhP-Tqvd5NxhoBE-4iWxRQ,9986
34
+ jettask/executor/process_entry.py,sha256=wr_0qXD5O_wZfkXRHRaW7ZJXZ97iWtPcEjt9c4njdnI,19510
35
+ jettask/executor/task_executor.py,sha256=RqHE6K0bcvZ72-Wkc5oGl41KIuFvZ--6Y8bqi35vtXg,11270
36
+ jettask/messaging/__init__.py,sha256=hWv1p5kmud8nTbx-KceKkmaYbvYWhzJWNR8OzTvoRP0,1961
37
+ jettask/messaging/event_pool.py,sha256=m2xacXiaflsTCRCaWWbH07_RHDezNgDYONY2EK_lm2I,118256
38
+ jettask/messaging/reader.py,sha256=hlOYzU1LJrm7OhKb5iBrUjqHjsN3sN5FGKYHLiHGHes,17556
39
+ jettask/messaging/registry.py,sha256=wG6SvyoCFMDggGd_SaDlLKaBcUPWzlblcGLMRhJwx90,10290
40
+ jettask/messaging/scanner.py,sha256=jRr2-RXfMNTLfU6v9ZX4QEPPBZdTqjwq9JSvMz8WkRQ,15496
41
+ jettask/messaging/sender.py,sha256=cz98GiSMQhKxUyEkPmOHUIG78GFMW_f-P4j-KUCVMrE,10051
42
+ jettask/messaging/pg_consumer/pg_consumer_v2.py,sha256=ajd6ZM_HY9YWK_I5BNbGuWxNqSS892d3ZzvDlTSQgwc,16211
43
+ jettask/messaging/pg_consumer/sql_utils.py,sha256=dZM_8kIzUmRq0me4yLwb3UeoDvIVKJdIc9VNP1nbS0E,6461
44
+ jettask/messaging/pg_consumer/sql/add_execution_time_field.sql,sha256=z2Jx-0uG-IW5RsTGxcLFu2_BcMReluEptMJWB6Qrj08,1184
45
+ jettask/messaging/pg_consumer/sql/create_new_tables.sql,sha256=layb08_MFGVSE-NQEcjlQBklZ307JtJaccrnqkcj7Vg,6566
46
+ jettask/messaging/pg_consumer/sql/create_tables_v3.sql,sha256=ZwBjxKWU6imWACoWMCIACLsYUPiPep9WmJsdIjI2a58,8173
47
+ jettask/messaging/pg_consumer/sql/migrate_to_new_structure.sql,sha256=elhObWvbtqAhb4osJC6D5JhxcOxhGJgIBqx4ItB_xO8,5716
48
+ jettask/messaging/pg_consumer/sql/modify_time_fields.sql,sha256=mRfoXbP9iXbAJBj-xNume8-4AV_UPQcBALNUqEf3aKw,2572
49
+ jettask/persistence/__init__.py,sha256=9vU0k8ANmQxMBsd0Vtj-gd_ZNQoS8jJ2Bl8Cj2AsWTE,1228
50
+ jettask/persistence/buffer.py,sha256=Pd0EBT23c2mSdpSG5j4PGguj1mdIpBjyx6kkAVHooz8,5347
51
+ jettask/persistence/consumer.py,sha256=Krt_xY2he88Ge08uYq_idEnFb-yD_523Z1lygOYX8Ek,15589
52
+ jettask/persistence/manager.py,sha256=gPjdVu135lDniBuceHyaRyX2HhFC9uNvbyUhiNeycYY,11660
53
+ jettask/persistence/persistence.py,sha256=S_GEIRAdLVM60uYF93WUBWe5H5LJ01ZvnuALNY8Mxkw,16494
54
+ jettask/scheduler/__init__.py,sha256=vlNbwGS0r3ednD73ZD9nM4kFdbPk8WMQHIsM3qfMP5A,856
55
+ jettask/scheduler/database.py,sha256=HA3mAD9IN-zpjD7Ps70V0-gTG5g3WHq225LW0fRvAEk,26736
56
+ jettask/scheduler/loader.py,sha256=KtDdKnn2cF1Qegv5fU-BAfeCRlJR89VcB75JzjawBM8,8808
57
+ jettask/scheduler/manager.py,sha256=MQmSN7okmxrBq-i10fj_H1yarcxWgDDLfVauEbMbSrU,13003
58
+ jettask/scheduler/models.py,sha256=sSp_ItOb6d4ZW0wGN9LxrR475I0dJSuF2u9gHjrZEWs,7497
59
+ jettask/scheduler/schedule.py,sha256=OE0z4NCa85LOQYT6eRm37rlhsgSPYh-rXG6Qfgq4aEE,5718
60
+ jettask/scheduler/scheduler.py,sha256=Zj0nqEvDp-QfS5xl03oKzkJWU3G4PHxIVfcV36BYSZw,27842
61
+ jettask/schemas/__init__.py,sha256=COaNqPbLi71ColUk8PsZVV8VSzdAiS7Ad1IkBh5IL9s,4329
62
+ jettask/schemas/alert.py,sha256=Od29QjDrnLN1Tw_F2rPFSGsqRvbBcO2hf1d8GO1_WEQ,4958
63
+ jettask/schemas/backlog.py,sha256=SJVzNG7Vwm1tOWW4aAZMGLwsOxfv1FlmCK6HfOoWTws,6600
64
+ jettask/schemas/common.py,sha256=e1lrLnOpxKT25FH7QnDQy38Qj4SGqd_cwHvRObTuRZw,6038
65
+ jettask/schemas/monitoring.py,sha256=zPfNv6YXw3uufj61PseQTaQkpwa9azgsnCGUaF4HNUk,8084
66
+ jettask/schemas/namespace.py,sha256=9kYRBXGBqGV6i8EBmB4PTAmTG0ex2xOffZaUgMIDxvw,10870
67
+ jettask/schemas/queue.py,sha256=qy8Ty8BseRPyXmfDOpFgojTvYhsaBAJnpzFK2ZSYF_s,4516
68
+ jettask/schemas/responses.py,sha256=StGoD6Y2pNtU6V9t1xpbHoSWjigk_YBddai09SYDwqM,16522
69
+ jettask/schemas/scheduled_task.py,sha256=MPAXvDeqmyQTWhL31yS94mRuzJWOV4njukp6BQrX6K4,7130
70
+ jettask/schemas/schema.sql,sha256=IY-vLzj3TYoR4p51ChLuSxzo4KU1PQNFNDutc7p0nHg,4490
71
+ jettask/schemas/task.py,sha256=niW2F_84HvstAjX3vo2pZswoRwMCF3DIoONuHwWNwtk,3172
72
+ jettask/task/__init__.py,sha256=IR3c7HJ3NK25YzwrlKBhpyUcbv5kGnDQ9ogzcmR1zdU,246
73
+ jettask/task/router.py,sha256=nyHR4SAAFsWbeQh3Hza5U5a_w7l0WyZgsbUki9TzzhM,5900
74
+ jettask/task/task_registry.py,sha256=HYq6S9XWvavhy7UJeOGQSTR5LF0V_vyACcZ12yIMS68,7987
75
+ jettask/task/task_center/__init__.py,sha256=Ed_D427tD59wTeqfbjckB-qKrCPnRLuzWovpQRW9uFs,132
76
+ jettask/task/task_center/client.py,sha256=BcJOaKAeUKDl75GQzoQdBkZdBiC4gPxLrNa5VbKC86I,8537
77
+ jettask/utils/__init__.py,sha256=qyE5dRmgC5qYWtjGUHpIid9LOG65oHcv-Nx1uptfIRI,960
78
+ jettask/utils/backlog_collector.py,sha256=U3_TGHtcD4CbKOyFkbOShht8wkEeCwJ38QiZTvDgEn0,2523
79
+ jettask/utils/db_init.py,sha256=9E7hk8V-AYgffJ3WNbt34_7TKR1enOF3kBgFTcv1o2E,15486
80
+ jettask/utils/error_handler.py,sha256=9iYHCInUkEkP9o07dUgPxQr4aWqR5FUhnNWLgB0TEqM,2794
81
+ jettask/utils/exception_hook.py,sha256=Fp0m71_jjmmZvyoELARSsPIvSVPCmMQMVC1FxMFOoHQ,1765
82
+ jettask/utils/file_watcher.py,sha256=r3Mgekb_5sOssDrnFBCbbyvpWkwD2ZPA_j22ztzRCT0,1207
83
+ jettask/utils/helpers.py,sha256=uTdyRFZsdwSQzuEcNoGgADuVnHaDEYq6JQP6zVxAj04,3368
84
+ jettask/utils/logger.py,sha256=PcZgUtx7I9pe90Y8WgPQtKhMefd1oQ14pgUlKCFpCXo,843
85
+ jettask/utils/namespace_dep.py,sha256=JVcBqRPYwhGtmWFg-OKoPkxkeYCU4IxsGjx6PRF_jqM,5168
86
+ jettask/utils/queue_matcher.py,sha256=AP7Zh1WDMh2vG8XB7JCMAaepAb37rGi0O2HuMgBJxng,5430
87
+ jettask/utils/redis_monitor.py,sha256=2l6fF2qAtctS9HUaEV3acJMJ5zhkvT4iygJGhGBCoKw,3660
88
+ jettask/utils/serializer.py,sha256=Omnnbpp9FuRDugqan2QK75kVV91Rv07kN-hzqCC_3G4,856
89
+ jettask/utils/stream_backlog.py,sha256=4F-AiRngdby9CW8evdwWGLYrMEeTbNrCFMrbfB7RHcg,15971
90
+ jettask/utils/task_logger.py,sha256=kauCgiDDzhuge3wk-BXR_KwytOfmrrVBKe1S4XUWBXU,13628
91
+ jettask/utils/time_sync.py,sha256=DcZWboLKa9DwyQgDGGZGNLrQrMRVaJXJLk0xCDUW0B4,5209
92
+ jettask/utils/traceback_filter.py,sha256=2svLo5_OoosAbS08UpceeWtjSKkdx-Fp6U-ysJQkroY,3025
93
+ jettask/utils/rate_limit/__init__.py,sha256=Ahl96s0BqSofOIsQUIHfc2QpO46GezjtLXC7QzOFpKs,810
94
+ jettask/utils/rate_limit/concurrency_limiter.py,sha256=NPHlPwhudoceobgIb2BsuKrLxUaOsZIF0G9lcZoKVDA,27718
95
+ jettask/utils/rate_limit/config.py,sha256=ChOQ2AWUBMbq3lTImYZ59UBwDPAetJ3UcV0WuhHPUgY,3862
96
+ jettask/utils/rate_limit/limiter.py,sha256=itH_APz7m5qBntJtlfF120OvkD1N7hKuiwPtu6OF28U,1285
97
+ jettask/utils/rate_limit/manager.py,sha256=R9dqczrJYpVhcecMASEYdnPDKmSVaNwmdnzrzLvadZg,9535
98
+ jettask/utils/rate_limit/qps_limiter.py,sha256=npo6_Sw51AT9I4OvRfsYZddViF6cZQl5DpW8XDWRRBo,4787
99
+ jettask/utils/rate_limit/task_limiter.py,sha256=7QaK1IR8rF-h1e-4-loxFL5tBiQPceFXLCdqcK8Rpl4,14072
100
+ jettask/webui/__init__.py,sha256=OcPwYBgPX1PSWF_Hdnxaq_2N2jX-VCdyrs-MYrwkmds,570
101
+ jettask/webui/app.py,sha256=vcQ7-gYnN8mf8z9BCMqzNuq11S9crd2eAQ12AU8CiNo,4657
102
+ jettask/webui/api/__init__.py,sha256=GYl42r7ZtGtqOtiFUeyrKsNDYKTo8jS03Pci-sQxqm8,1200
103
+ jettask/webui/api/alerts.py,sha256=Vy9PwihQyaFtoFXeuyxNZcVQxJpIR1HIAkVYzpj37hY,29540
104
+ jettask/webui/api/example_refactored.py,sha256=aVaGDuIrrdlfjmrXnfusWWCRz4SdDGIj4CxCBDcUY5Y,13603
105
+ jettask/webui/api/namespaces.py,sha256=hY3tSplUjteQdITjKdgrd0X06T8k8-EAPW29lQSW-zQ,14637
106
+ jettask/webui/api/overview.py,sha256=tyf9xlW7r-ztnZDmgacih_QUNhR7DFhwpTrpasT8ZdQ,12014
107
+ jettask/webui/api/queues.py,sha256=SU1mjf3VL6qL3biWAQVkX9NhHwcgoSZ-GJuPQH_LQeQ,42341
108
+ jettask/webui/api/scheduled.py,sha256=XKqgnRwrRrMZ1zGtdO0x6aKMBOwBEy1-2BXdDzvSE6o,54370
109
+ jettask/webui/api/settings.py,sha256=vPg1p8TkNK0E9tml6nZY8yxdUo4zJ0pL2s0eqBjuPaA,4435
110
+ jettask/webui/api/workers.py,sha256=pK7G5bvFic5Nhgot1HqhJBtcygUkMS34RnENldHyUHg,14101
111
+ jettask/webui/middleware/__init__.py,sha256=v_5RM0XlGEYL4VGiPG8Jy12bIkOtHzhkkXXzh_uGJh0,119
112
+ jettask/webui/middleware/namespace_middleware.py,sha256=kZ4k1rnlOibuYzhSejcBc_G5X5nmh6vK3rndJZXHnd4,4926
113
+ jettask/webui/services/__init__.py,sha256=PyUMoa6tEEYL7VFJkblIB7r_4ojr4vNls3qUfvx3ooU,6109
114
+ jettask/webui/services/alert_service.py,sha256=AIk4hjx-yoFnl5vPff7TMwjgjheKu91da9mZ5gYDroo,14169
115
+ jettask/webui/services/analytics_service.py,sha256=zLTtGUJ7wYIU7O5NP_VS_-wk_IXNfxKikAvQ3aaeCmI,1101
116
+ jettask/webui/services/heartbeat_service.py,sha256=Srp3IoVJaoz2EF-dobH9Eq0bSVJGz8s9LEZlT1ByxyY,9386
117
+ jettask/webui/services/overview_service.py,sha256=kuNz-AxRFa_z6bAytA05J797MNBHYlp2qvQbMjBOgEA,37102
118
+ jettask/webui/services/queue_monitor_service.py,sha256=XkJuwFrVO3GqKcfC9Ll3d-XV2Nxt1bISAfJJF_RBuZw,14927
119
+ jettask/webui/services/queue_service.py,sha256=3ONDfGos2xHtHZlWcAlaXzEo1Km-HTIllCpN3GPKKG0,27419
120
+ jettask/webui/services/queue_stats_v2.py,sha256=DW46_w1-3k9PozZScLUen0-wbHMTgxo346DLVmxsw5Q,24409
121
+ jettask/webui/services/redis_monitor_service.py,sha256=Ot1POh4GS3qbFMx03kjDDox53tnINxTepcUQmt8KfTU,2524
122
+ jettask/webui/services/scheduled_task_service.py,sha256=ZFFtcUdk_IDGm17maHA1Hb0p00_56QqQ9ETFAF7T1bU,6226
123
+ jettask/webui/services/settings_service.py,sha256=nyeTPF0DaLtNfk6h2uHfzIzf7L53GV9zhfL4bstxE_Q,35592
124
+ jettask/webui/services/task_monitor_service.py,sha256=lLWGi9eO1gmemV0Wxa2h5S1uP0J5gXAGYYUb6ZMiJUU,7804
125
+ jettask/webui/services/task_service.py,sha256=vfSEyhFxDcEzOuA4sHEg1sYvLriUUvJ08SQ2ICG7MSU,4751
126
+ jettask/webui/services/timeline_pg_service.py,sha256=6IScSTnK_ZCzg7_EGs0CLrac1RTqLzhp7KsrnPjsOFM,17467
127
+ jettask/webui/services/timeline_service.py,sha256=79QqB7nAQGj3vHl1Ubf1ao7ALRFahiaPr2px4wHCnOI,6434
128
+ jettask/webui/services/worker_monitor_service.py,sha256=KTjJsMLHrHKIRYWi9RHKNjIXIHzMNAC-nqdXh0XdSnM,17323
129
+ jettask/webui/sql/batch_upsert_functions.sql,sha256=5eWOhOD8gWHhtcop_BrCpZTxPFeyBHtt_leNQZO89Cs,6615
130
+ jettask/webui/sql/verify_database.sql,sha256=HtTup3xHWbOo1BTU_u4i41E9LrPEXB8qYbChL9WeWOc,2313
131
+ jettask/webui/utils/__init__.py,sha256=4Ne5wAOwejOzKZVk6DRQ6apOI9mTS_tu9TOvKAFL5gk,208
132
+ jettask/webui/utils/time_utils.py,sha256=UwBkt0ZKH5wpDlRZx6wvTlacSlqAbMQ9cIhlDZnArgc,3426
133
+ jettask/worker/__init__.py,sha256=HUwUjZEWV6oaGJSUg26PPwVsIacAB8Tk8qzQQYvlMLM,1218
134
+ jettask/worker/lifecycle.py,sha256=ioSIw2TidBnGpHrryIzQE4cgSDjiUAlClPBUtrAlf1s,58989
135
+ jettask/worker/manager.py,sha256=fIayGpH9nLqvxTv-hDwrVj41sQqid96ZzNjENCLR1Nk,19913
136
+ jettask/worker/recovery.py,sha256=I7Au9DPpsEhlILxYcnnC0H-Qw51rr97sUsy2pn43Czw,29120
137
+ jettask-0.2.24.dist-info/licenses/LICENSE,sha256=sKR8OPWvnqxzcHAmnaVSANpRpeM0Z52PNLCB0ZlFN6c,1062
138
+ jettask-0.2.24.dist-info/METADATA,sha256=Yd9LfUdxKRpsHlYmSx2vvqG0gd0GxhHm8pqbqVWbAWY,1949
139
+ jettask-0.2.24.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
140
+ jettask-0.2.24.dist-info/entry_points.txt,sha256=9-8eTLJPgzpBgeGC8WHdMYtldZxHAXFspMeqArVUdew,148
141
+ jettask-0.2.24.dist-info/top_level.txt,sha256=uymyRUF87-OsSurk5NhpeTW0jy3Wltnn91Zoa6jmAaw,8
142
+ jettask-0.2.24.dist-info/RECORD,,
@@ -1,338 +0,0 @@
1
- """
2
- 统一执行器
3
-
4
- 整合了单进程和多进程执行模式的统一接口
5
- """
6
-
7
- import asyncio
8
- import multiprocessing
9
- import logging
10
- import os
11
- import time
12
- from collections import deque
13
- from typing import List, Optional
14
-
15
- from .core import ExecutionMode, ExecutorCore
16
- from .orchestrator import ProcessConfig, ProcessOrchestrator
17
- from ..worker.lifecycle import WorkerStateManager
18
- from ..utils.rate_limit.manager import RateLimiterManager
19
-
20
- logger = logging.getLogger('app')
21
-
22
- # Try to use uvloop for better performance
23
- try:
24
- import uvloop
25
- uvloop.install()
26
- logger.debug("Using uvloop for better performance")
27
- except ImportError:
28
- pass
29
-
30
-
31
- class UnifiedExecutor:
32
- """
33
- 统一执行器
34
-
35
- 整合AsyncioExecutor和MultiAsyncioExecutor的功能
36
- 支持单进程和多进程两种执行模式
37
-
38
- 职责:
39
- 1. 提供统一的执行器接口
40
- 2. 根据模式选择ExecutorCore或ProcessOrchestrator
41
- 3. 管理事件队列和任务分发
42
- """
43
-
44
- def __init__(self, event_queue, app, concurrency=100,
45
- mode: ExecutionMode = ExecutionMode.SINGLE_PROCESS,
46
- task_name: str = None):
47
- """
48
- 初始化统一执行器
49
-
50
- Args:
51
- event_queue: 事件队列
52
- app: Application实例
53
- concurrency: 并发数
54
- mode: 执行模式
55
- task_name: 任务名称(单进程模式必需)
56
- """
57
- self.event_queue = event_queue
58
- self.app = app
59
- self.concurrency = concurrency
60
- self.mode = mode
61
- self.task_name = task_name
62
-
63
- # 根据模式初始化核心组件
64
- if mode == ExecutionMode.SINGLE_PROCESS:
65
- if not task_name:
66
- raise ValueError("task_name is required for SINGLE_PROCESS mode")
67
-
68
- self.executor_core = ExecutorCore(
69
- app=app,
70
- task_name=task_name,
71
- concurrency=concurrency
72
- )
73
- self.orchestrator = None
74
- logger.debug(f"UnifiedExecutor initialized in SINGLE_PROCESS mode for task {task_name}")
75
-
76
- elif mode == ExecutionMode.MULTI_PROCESS:
77
- self.executor_core = None
78
- self.orchestrator = ProcessOrchestrator(
79
- app=app,
80
- num_processes=concurrency
81
- )
82
- logger.debug(f"UnifiedExecutor initialized in MULTI_PROCESS mode with {concurrency} processes")
83
-
84
- else:
85
- raise ValueError(f"Unsupported execution mode: {mode}")
86
-
87
- # 活动任务集合(单进程模式使用)
88
- self._active_tasks = set()
89
-
90
- def logic(self, *args, **kwargs):
91
- """
92
- BaseExecutor接口方法
93
- 在单进程模式下不使用,多进程模式委托给ProcessOrchestrator
94
- """
95
- pass
96
-
97
- async def loop(self):
98
- """主循环 - 单进程模式"""
99
- if self.mode != ExecutionMode.SINGLE_PROCESS:
100
- raise RuntimeError("loop() is only for SINGLE_PROCESS mode")
101
-
102
- # 初始化限流器
103
- self.app.consumer_manager._heartbeat_strategy._ensure_consumer_id()
104
- worker_id = self.app.consumer_manager._heartbeat_strategy.consumer_id
105
- registry_manager = self.app.consumer_manager
106
-
107
- if not self.app.worker_state_manager:
108
- self.app.worker_state_manager = WorkerStateManager(
109
- redis_client=self.app.ep.async_redis_client,
110
- redis_prefix=self.executor_core.prefix,
111
- event_pool=self.app.ep # 传入 EventPool 实例,启用事件驱动的消息恢复
112
- )
113
- await self.app.worker_state_manager.start_listener()
114
- logger.debug(f"WorkerStateManager started for worker {worker_id}")
115
-
116
- # # 初始化时间同步
117
- from jettask.utils.time_sync import init_time_sync
118
- time_sync = await init_time_sync(self.app.ep.async_redis_client)
119
- logger.debug(f"TimeSync initialized, offset={time_sync.get_offset():.6f}s")
120
-
121
- self.executor_core.rate_limiter_manager = RateLimiterManager(
122
- redis_client=self.app.ep.async_redis_client,
123
- worker_id=worker_id,
124
- redis_prefix=self.executor_core.prefix,
125
- registry_manager=registry_manager,
126
- worker_state_manager=self.app.worker_state_manager
127
- )
128
- logger.debug(f"RateLimiterManager initialized for worker {worker_id}")
129
-
130
- await self.executor_core.rate_limiter_manager.load_config_from_redis()
131
-
132
- tasks_batch = []
133
- max_buffer_size = 5000
134
-
135
- try:
136
- while True:
137
- # 检查退出信号
138
- if hasattr(self.app, '_should_exit') and self.app._should_exit:
139
- logger.debug("UnifiedExecutor detected shutdown signal")
140
- break
141
-
142
- # 检查父进程
143
- if hasattr(os, 'getppid') and os.getppid() == 1:
144
- logger.debug("Parent process died, exiting...")
145
- break
146
-
147
- current_time = time.time()
148
-
149
- # 获取事件
150
- event = None
151
- try:
152
- event = await asyncio.wait_for(self.event_queue.get(), timeout=0.1)
153
- except asyncio.TimeoutError:
154
- event = None
155
-
156
- if event:
157
- event.pop("execute_time", None)
158
- tasks_batch.append(event)
159
- logger.debug(f"[EVENT] Got event: {event.get('event_id', 'unknown')}, task_name={event.get('event_data', {}).get('_task_name')}")
160
-
161
- # 批量创建任务
162
- if tasks_batch:
163
- for event in tasks_batch:
164
- event_data = event.get('event_data', {})
165
- event_task_name = event_data.get("_task_name") or event_data.get("name")
166
-
167
- if not event_task_name:
168
- logger.error(f"No task_name in event {event.get('event_id')}")
169
- continue
170
-
171
- # 验证任务名称匹配
172
- if event_task_name != self.task_name:
173
- logger.error(f"Task name mismatch: {event_task_name} != {self.task_name}")
174
- continue
175
-
176
- # 限流控制
177
- logger.debug(f"[TASK] Attempting to acquire rate limit for {self.task_name}, event_id={event.get('event_id')}")
178
- rate_limit_token = await self.executor_core.rate_limiter_manager.acquire(
179
- task_name=self.task_name,
180
- timeout=None
181
- )
182
- print(f'{rate_limit_token=}')
183
- if not rate_limit_token:
184
- logger.error(f"Failed to acquire token for {self.task_name}")
185
- continue
186
- logger.debug(f"[TASK] Successfully acquired rate limit for {self.task_name}, token={rate_limit_token}, starting execution")
187
-
188
- self.executor_core.batch_counter += 1
189
-
190
- # 创建任务包装器,在任务完成时自动释放限流许可
191
- async def execute_with_release(event_data, token):
192
- try:
193
- await self.executor_core.execute_task(**event_data)
194
- finally:
195
- # 无论任务成功还是失败,都释放并发许可
196
- await self.executor_core.rate_limiter_manager.release(self.task_name, task_id=token)
197
-
198
- task = asyncio.create_task(execute_with_release(event, rate_limit_token))
199
- self._active_tasks.add(task)
200
- task.add_done_callback(self._active_tasks.discard)
201
-
202
- tasks_batch.clear()
203
-
204
- # 智能缓冲区管理
205
- buffer_full = (
206
- len(self.executor_core.pending_acks) >= max_buffer_size or
207
- len(self.executor_core.status_updates) >= max_buffer_size or
208
- len(self.executor_core.data_updates) >= max_buffer_size or
209
- len(self.executor_core.task_info_updates) >= max_buffer_size
210
- )
211
-
212
- should_flush_periodic = False
213
- has_pending_data = (
214
- self.executor_core.pending_acks or
215
- self.executor_core.status_updates or
216
- self.executor_core.data_updates or
217
- self.executor_core.task_info_updates
218
- )
219
-
220
- if has_pending_data:
221
- for data_type, config in self.executor_core.pipeline_config.items():
222
- time_since_flush = current_time - self.executor_core.last_pipeline_flush[data_type]
223
-
224
- if data_type == 'ack' and self.executor_core.pending_acks:
225
- if time_since_flush >= config['max_delay']:
226
- should_flush_periodic = True
227
- break
228
- elif data_type == 'task_info' and self.executor_core.task_info_updates:
229
- if time_since_flush >= config['max_delay']:
230
- should_flush_periodic = True
231
- break
232
- elif data_type == 'status' and self.executor_core.status_updates:
233
- if time_since_flush >= config['max_delay']:
234
- should_flush_periodic = True
235
- break
236
- elif data_type == 'data' and self.executor_core.data_updates:
237
- if time_since_flush >= config['max_delay']:
238
- should_flush_periodic = True
239
- break
240
-
241
- if buffer_full or should_flush_periodic:
242
- asyncio.create_task(self.executor_core._flush_all_buffers())
243
-
244
- # 智能休眠
245
- has_events = False
246
- if isinstance(self.event_queue, deque):
247
- has_events = bool(self.event_queue)
248
- elif isinstance(self.event_queue, asyncio.Queue):
249
- has_events = not self.event_queue.empty()
250
-
251
- if has_events:
252
- await asyncio.sleep(0)
253
- else:
254
- if has_pending_data:
255
- await self.executor_core._flush_all_buffers()
256
- await asyncio.sleep(0.001)
257
-
258
- except KeyboardInterrupt:
259
- logger.debug("UnifiedExecutor received KeyboardInterrupt")
260
- except Exception as e:
261
- logger.error(f"UnifiedExecutor loop error: {e}")
262
- finally:
263
- await self._cleanup_single_process()
264
-
265
- async def _cleanup_single_process(self):
266
- """清理单进程模式资源"""
267
- logger.debug("UnifiedExecutor cleaning up...")
268
-
269
- # 设置停止标志
270
- if hasattr(self.app.ep, '_stop_reading'):
271
- self.app.ep._stop_reading = True
272
-
273
- # 取消活动任务
274
- if self._active_tasks:
275
- logger.debug(f"Cancelling {len(self._active_tasks)} active tasks...")
276
- for task in self._active_tasks:
277
- if not task.done():
278
- task.cancel()
279
-
280
- if self._active_tasks:
281
- try:
282
- await asyncio.wait_for(
283
- asyncio.gather(*self._active_tasks, return_exceptions=True),
284
- timeout=0.2
285
- )
286
- except asyncio.TimeoutError:
287
- logger.debug("Some tasks did not complete in time")
288
-
289
- # 清理ExecutorCore
290
- await self.executor_core.cleanup()
291
-
292
- # 清理event_pool
293
- if hasattr(self.app.ep, 'cleanup'):
294
- try:
295
- self.app.ep.cleanup()
296
- except Exception as e:
297
- logger.error(f"Error cleaning up EventPool: {e}")
298
-
299
- # 标记worker离线
300
- if self.app.consumer_manager:
301
- try:
302
- self.app.consumer_manager.cleanup()
303
- logger.debug("Worker marked as offline")
304
- except Exception as e:
305
- logger.error(f"Error marking worker offline: {e}")
306
-
307
- logger.debug("UnifiedExecutor stopped")
308
-
309
- def start_multi_process(self, queues: List[str], prefetch_multiplier: int = 100, worker_id: str = None, worker_key: str = None):
310
- """启动多进程模式
311
-
312
- Args:
313
- queues: 队列列表
314
- prefetch_multiplier: 预取倍数
315
- worker_id: Worker ID(主进程生成,子进程复用)
316
- worker_key: Worker Key(主进程生成,子进程复用)
317
- """
318
- if self.mode != ExecutionMode.MULTI_PROCESS:
319
- raise RuntimeError("start_multi_process() is only for MULTI_PROCESS mode")
320
-
321
- self.orchestrator.start(queues, prefetch_multiplier, worker_id, worker_key)
322
-
323
- def shutdown(self):
324
- """
325
- 关闭执行器
326
-
327
- 根据执行模式调用相应的关闭方法
328
- """
329
- if self.mode == ExecutionMode.MULTI_PROCESS:
330
- if self.orchestrator:
331
- self.orchestrator.shutdown()
332
- elif self.mode == ExecutionMode.SINGLE_PROCESS:
333
- # 单进程模式的清理在 _cleanup_single_process 中处理
334
- # 这里只是一个占位符,实际清理由事件循环完成
335
- logger.debug("UnifiedExecutor shutdown called in SINGLE_PROCESS mode")
336
-
337
-
338
- __all__ = ['UnifiedExecutor']