jettask 0.2.5__py3-none-any.whl → 0.2.6__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/monitor/run_backlog_collector.py +96 -0
- jettask/monitor/stream_backlog_monitor.py +362 -0
- jettask/pg_consumer/pg_consumer_v2.py +403 -0
- jettask/pg_consumer/sql_utils.py +182 -0
- jettask/scheduler/__init__.py +17 -0
- jettask/scheduler/add_execution_count.sql +11 -0
- jettask/scheduler/add_priority_field.sql +26 -0
- jettask/scheduler/add_scheduler_id.sql +25 -0
- jettask/scheduler/add_scheduler_id_index.sql +10 -0
- jettask/scheduler/loader.py +249 -0
- jettask/scheduler/make_scheduler_id_required.sql +28 -0
- jettask/scheduler/manager.py +696 -0
- jettask/scheduler/migrate_interval_seconds.sql +9 -0
- jettask/scheduler/models.py +200 -0
- jettask/scheduler/multi_namespace_scheduler.py +294 -0
- jettask/scheduler/performance_optimization.sql +45 -0
- jettask/scheduler/run_scheduler.py +186 -0
- jettask/scheduler/scheduler.py +715 -0
- jettask/scheduler/schema.sql +84 -0
- jettask/scheduler/unified_manager.py +450 -0
- jettask/scheduler/unified_scheduler_manager.py +280 -0
- jettask/webui/backend/api/__init__.py +3 -0
- jettask/webui/backend/api/v1/__init__.py +17 -0
- jettask/webui/backend/api/v1/monitoring.py +431 -0
- jettask/webui/backend/api/v1/namespaces.py +504 -0
- jettask/webui/backend/api/v1/queues.py +342 -0
- jettask/webui/backend/api/v1/tasks.py +367 -0
- jettask/webui/backend/core/__init__.py +3 -0
- jettask/webui/backend/core/cache.py +221 -0
- jettask/webui/backend/core/database.py +200 -0
- jettask/webui/backend/core/exceptions.py +102 -0
- jettask/webui/backend/models/__init__.py +3 -0
- jettask/webui/backend/models/requests.py +236 -0
- jettask/webui/backend/models/responses.py +230 -0
- jettask/webui/backend/services/__init__.py +3 -0
- jettask/webui/frontend/index.html +13 -0
- jettask/webui/models/__init__.py +3 -0
- jettask/webui/models/namespace.py +63 -0
- jettask/webui/sql/batch_upsert_functions.sql +178 -0
- jettask/webui/sql/init_database.sql +640 -0
- {jettask-0.2.5.dist-info → jettask-0.2.6.dist-info}/METADATA +11 -9
- {jettask-0.2.5.dist-info → jettask-0.2.6.dist-info}/RECORD +46 -53
- jettask/webui/frontend/package-lock.json +0 -4833
- jettask/webui/frontend/package.json +0 -30
- jettask/webui/frontend/src/App.css +0 -109
- jettask/webui/frontend/src/App.jsx +0 -66
- jettask/webui/frontend/src/components/NamespaceSelector.jsx +0 -166
- jettask/webui/frontend/src/components/QueueBacklogChart.jsx +0 -298
- jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +0 -638
- jettask/webui/frontend/src/components/QueueDetailsTable.css +0 -65
- jettask/webui/frontend/src/components/QueueDetailsTable.jsx +0 -487
- jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +0 -465
- jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +0 -423
- jettask/webui/frontend/src/components/TaskFilter.jsx +0 -425
- jettask/webui/frontend/src/components/TimeRangeSelector.css +0 -21
- jettask/webui/frontend/src/components/TimeRangeSelector.jsx +0 -160
- jettask/webui/frontend/src/components/charts/QueueChart.jsx +0 -111
- jettask/webui/frontend/src/components/charts/QueueTrendChart.jsx +0 -115
- jettask/webui/frontend/src/components/charts/WorkerChart.jsx +0 -40
- jettask/webui/frontend/src/components/common/StatsCard.jsx +0 -18
- jettask/webui/frontend/src/components/layout/AppLayout.css +0 -95
- jettask/webui/frontend/src/components/layout/AppLayout.jsx +0 -49
- jettask/webui/frontend/src/components/layout/Header.css +0 -106
- jettask/webui/frontend/src/components/layout/Header.jsx +0 -106
- jettask/webui/frontend/src/components/layout/SideMenu.css +0 -137
- jettask/webui/frontend/src/components/layout/SideMenu.jsx +0 -209
- jettask/webui/frontend/src/components/layout/TabsNav.css +0 -244
- jettask/webui/frontend/src/components/layout/TabsNav.jsx +0 -206
- jettask/webui/frontend/src/components/layout/UserInfo.css +0 -197
- jettask/webui/frontend/src/components/layout/UserInfo.jsx +0 -197
- jettask/webui/frontend/src/contexts/LoadingContext.jsx +0 -27
- jettask/webui/frontend/src/contexts/NamespaceContext.jsx +0 -72
- jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +0 -245
- jettask/webui/frontend/src/index.css +0 -114
- jettask/webui/frontend/src/main.jsx +0 -20
- jettask/webui/frontend/src/pages/Alerts.jsx +0 -684
- jettask/webui/frontend/src/pages/Dashboard/index.css +0 -35
- jettask/webui/frontend/src/pages/Dashboard/index.jsx +0 -281
- jettask/webui/frontend/src/pages/Dashboard.jsx +0 -1330
- jettask/webui/frontend/src/pages/QueueDetail.jsx +0 -1117
- jettask/webui/frontend/src/pages/QueueMonitor.jsx +0 -527
- jettask/webui/frontend/src/pages/Queues.jsx +0 -12
- jettask/webui/frontend/src/pages/ScheduledTasks.jsx +0 -809
- jettask/webui/frontend/src/pages/Settings.jsx +0 -800
- jettask/webui/frontend/src/pages/Workers.jsx +0 -12
- jettask/webui/frontend/src/services/api.js +0 -114
- jettask/webui/frontend/src/services/queueTrend.js +0 -152
- jettask/webui/frontend/src/utils/suppressWarnings.js +0 -22
- jettask/webui/frontend/src/utils/userPreferences.js +0 -154
- {jettask-0.2.5.dist-info → jettask-0.2.6.dist-info}/WHEEL +0 -0
- {jettask-0.2.5.dist-info → jettask-0.2.6.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.5.dist-info → jettask-0.2.6.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.5.dist-info → jettask-0.2.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,249 @@
|
|
1
|
+
"""
|
2
|
+
任务加载器 - 负责从数据库加载任务到Redis
|
3
|
+
"""
|
4
|
+
import asyncio
|
5
|
+
from redis import asyncio as aioredis
|
6
|
+
from typing import Optional, Set
|
7
|
+
from datetime import datetime, timedelta
|
8
|
+
import json
|
9
|
+
|
10
|
+
from ..utils.task_logger import get_task_logger, LogContext
|
11
|
+
from .manager import ScheduledTaskManager
|
12
|
+
from .models import ScheduledTask
|
13
|
+
|
14
|
+
|
15
|
+
logger = get_task_logger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
class TaskLoader:
|
19
|
+
"""
|
20
|
+
任务加载器
|
21
|
+
定期从数据库加载即将执行的任务到Redis ZSET
|
22
|
+
"""
|
23
|
+
|
24
|
+
def __init__(
|
25
|
+
self,
|
26
|
+
redis_url: str,
|
27
|
+
db_manager: ScheduledTaskManager,
|
28
|
+
redis_prefix: str = "jettask:scheduled",
|
29
|
+
lookahead_minutes: int = 5,
|
30
|
+
load_interval: int = 30,
|
31
|
+
sync_interval_cycles: int = 2 # 每几个周期同步一次
|
32
|
+
):
|
33
|
+
"""
|
34
|
+
初始化加载器
|
35
|
+
|
36
|
+
Args:
|
37
|
+
redis_url: Redis连接URL
|
38
|
+
db_manager: 数据库管理器
|
39
|
+
redis_prefix: Redis键前缀
|
40
|
+
lookahead_minutes: 向前查看的分钟数(加载未来N分钟的任务)
|
41
|
+
load_interval: 加载间隔(秒)
|
42
|
+
sync_interval_cycles: 每几个周期与数据库同步一次(默认2个周期=1分钟)
|
43
|
+
"""
|
44
|
+
self.sync_interval_cycles = sync_interval_cycles
|
45
|
+
self.redis_url = redis_url
|
46
|
+
self.db_manager = db_manager
|
47
|
+
self.redis_prefix = redis_prefix
|
48
|
+
self.lookahead_minutes = lookahead_minutes
|
49
|
+
self.load_interval = load_interval
|
50
|
+
|
51
|
+
self.redis: Optional[aioredis.Redis] = None
|
52
|
+
self.running = False
|
53
|
+
self.loaded_tasks: Set[str] = set() # 已加载的任务ID
|
54
|
+
|
55
|
+
async def connect(self):
|
56
|
+
"""建立Redis连接"""
|
57
|
+
if not self.redis:
|
58
|
+
self.redis = await aioredis.from_url(
|
59
|
+
self.redis_url,
|
60
|
+
encoding="utf-8",
|
61
|
+
decode_responses=False
|
62
|
+
)
|
63
|
+
|
64
|
+
async def disconnect(self):
|
65
|
+
"""关闭Redis连接"""
|
66
|
+
if self.redis:
|
67
|
+
await self.redis.close()
|
68
|
+
self.redis = None
|
69
|
+
|
70
|
+
def _get_zset_key(self) -> str:
|
71
|
+
"""获取ZSET键名"""
|
72
|
+
return f"{self.redis_prefix}:tasks"
|
73
|
+
|
74
|
+
def _get_task_detail_key(self, task_id: int) -> str:
|
75
|
+
"""获取任务详情键名"""
|
76
|
+
return f"{self.redis_prefix}:task:{task_id}"
|
77
|
+
|
78
|
+
async def load_tasks(self) -> int:
|
79
|
+
"""
|
80
|
+
从数据库加载任务到Redis
|
81
|
+
|
82
|
+
Returns:
|
83
|
+
加载的任务数量
|
84
|
+
"""
|
85
|
+
with LogContext(operation="load_tasks"):
|
86
|
+
try:
|
87
|
+
# 获取即将执行的任务
|
88
|
+
tasks = await self.db_manager.get_ready_tasks(
|
89
|
+
batch_size=1000,
|
90
|
+
lookahead_seconds=self.lookahead_minutes * 60
|
91
|
+
)
|
92
|
+
|
93
|
+
if not tasks:
|
94
|
+
logger.debug("No tasks to load")
|
95
|
+
return 0
|
96
|
+
|
97
|
+
# 批量添加到Redis
|
98
|
+
pipe = self.redis.pipeline()
|
99
|
+
loaded_count = 0
|
100
|
+
|
101
|
+
for task in tasks:
|
102
|
+
if not task.enabled or not task.next_run_time:
|
103
|
+
continue
|
104
|
+
|
105
|
+
task_id = task.id
|
106
|
+
score = task.next_run_time.timestamp()
|
107
|
+
|
108
|
+
# 添加到ZSET(用于调度)
|
109
|
+
pipe.zadd(self._get_zset_key(), {str(task_id): score}) # Redis ZSET 需要字符串键
|
110
|
+
|
111
|
+
# 存储任务详情
|
112
|
+
pipe.setex(
|
113
|
+
self._get_task_detail_key(task_id),
|
114
|
+
self.lookahead_minutes * 60 + 60, # 过期时间比lookahead稍长
|
115
|
+
task.to_redis_value()
|
116
|
+
)
|
117
|
+
|
118
|
+
self.loaded_tasks.add(task_id)
|
119
|
+
loaded_count += 1
|
120
|
+
|
121
|
+
await pipe.execute()
|
122
|
+
|
123
|
+
logger.info(f"Loaded {loaded_count} tasks to Redis",
|
124
|
+
extra={'extra_fields': {'task_count': loaded_count}})
|
125
|
+
|
126
|
+
return loaded_count
|
127
|
+
|
128
|
+
except Exception as e:
|
129
|
+
logger.error(f"Failed to load tasks: {e}", exc_info=True)
|
130
|
+
raise
|
131
|
+
|
132
|
+
async def remove_task(self, task_id: int):
|
133
|
+
"""
|
134
|
+
从Redis中移除任务
|
135
|
+
|
136
|
+
Args:
|
137
|
+
task_id: 任务ID
|
138
|
+
"""
|
139
|
+
pipe = self.redis.pipeline()
|
140
|
+
pipe.zrem(self._get_zset_key(), str(task_id))
|
141
|
+
pipe.delete(self._get_task_detail_key(task_id))
|
142
|
+
await pipe.execute()
|
143
|
+
|
144
|
+
self.loaded_tasks.discard(task_id)
|
145
|
+
|
146
|
+
logger.info(f"Removed task {task_id} from Redis")
|
147
|
+
|
148
|
+
async def update_task_score(self, task_id: int, next_run_time: datetime):
|
149
|
+
"""
|
150
|
+
更新任务在ZSET中的分数(下次执行时间)
|
151
|
+
|
152
|
+
Args:
|
153
|
+
task_id: 任务ID
|
154
|
+
next_run_time: 下次执行时间
|
155
|
+
"""
|
156
|
+
score = next_run_time.timestamp()
|
157
|
+
await self.redis.zadd(self._get_zset_key(), {str(task_id): score})
|
158
|
+
|
159
|
+
logger.debug(f"Updated task {task_id} score to {score}")
|
160
|
+
|
161
|
+
async def cleanup_expired(self):
|
162
|
+
"""清理过期的任务"""
|
163
|
+
# 移除已过期很久的任务(比如1小时前的)
|
164
|
+
cutoff_time = datetime.now() - timedelta(hours=1)
|
165
|
+
cutoff_score = cutoff_time.timestamp()
|
166
|
+
|
167
|
+
removed = await self.redis.zremrangebyscore(
|
168
|
+
self._get_zset_key(),
|
169
|
+
'-inf',
|
170
|
+
cutoff_score
|
171
|
+
)
|
172
|
+
|
173
|
+
if removed:
|
174
|
+
logger.info(f"Cleaned up {removed} expired tasks from Redis")
|
175
|
+
|
176
|
+
async def sync_with_db(self):
|
177
|
+
"""
|
178
|
+
与数据库同步任务状态
|
179
|
+
检查Redis中的任务是否仍然有效
|
180
|
+
"""
|
181
|
+
# 获取Redis中所有任务ID
|
182
|
+
redis_tasks = await self.redis.zrange(self._get_zset_key(), 0, -1)
|
183
|
+
redis_task_ids = {int(task_id.decode() if isinstance(task_id, bytes) else task_id)
|
184
|
+
for task_id in redis_tasks} # 转换为整数
|
185
|
+
|
186
|
+
# 批量检查这些任务在数据库中的状态
|
187
|
+
for task_id in redis_task_ids:
|
188
|
+
db_task = await self.db_manager.get_task(task_id) # task_id 现在是整数
|
189
|
+
|
190
|
+
if not db_task or not db_task.enabled:
|
191
|
+
# 任务已删除或禁用,从Redis移除
|
192
|
+
await self.remove_task(task_id)
|
193
|
+
elif db_task.next_run_time:
|
194
|
+
# 更新执行时间
|
195
|
+
await self.update_task_score(task_id, db_task.next_run_time)
|
196
|
+
|
197
|
+
async def run(self):
|
198
|
+
"""运行加载器主循环"""
|
199
|
+
self.running = True
|
200
|
+
logger.info("Task loader started")
|
201
|
+
|
202
|
+
# 主循环
|
203
|
+
cycle_count = 0
|
204
|
+
first_run = True
|
205
|
+
|
206
|
+
while self.running:
|
207
|
+
try:
|
208
|
+
# 确定本次循环的类型
|
209
|
+
if first_run:
|
210
|
+
cycle_type = "initial_load"
|
211
|
+
logger.info("Performing initial task load...")
|
212
|
+
else:
|
213
|
+
cycle_type = "load"
|
214
|
+
|
215
|
+
with LogContext(cycle=cycle_type):
|
216
|
+
# 加载任务
|
217
|
+
await self.load_tasks()
|
218
|
+
|
219
|
+
# 初始加载时执行同步
|
220
|
+
if first_run:
|
221
|
+
await self.sync_with_db()
|
222
|
+
first_run = False
|
223
|
+
else:
|
224
|
+
cycle_count += 1
|
225
|
+
|
226
|
+
# 每2个周期同步一次数据库(默认1分钟)
|
227
|
+
if cycle_count % self.sync_interval_cycles == 0:
|
228
|
+
await self.sync_with_db()
|
229
|
+
|
230
|
+
# 每5个周期清理一次过期任务
|
231
|
+
if cycle_count % 5 == 0:
|
232
|
+
await self.cleanup_expired()
|
233
|
+
|
234
|
+
# 重置计数器,避免溢出
|
235
|
+
if cycle_count >= 100:
|
236
|
+
cycle_count = 0
|
237
|
+
|
238
|
+
except Exception as e:
|
239
|
+
logger.error(f"Loader cycle error: {e}", exc_info=True)
|
240
|
+
|
241
|
+
# 等待间隔(放在循环末尾)
|
242
|
+
if self.running: # 只有在继续运行时才等待
|
243
|
+
await asyncio.sleep(self.load_interval)
|
244
|
+
|
245
|
+
logger.info("Task loader stopped")
|
246
|
+
|
247
|
+
def stop(self):
|
248
|
+
"""停止加载器"""
|
249
|
+
self.running = False
|
@@ -0,0 +1,28 @@
|
|
1
|
+
-- Migration to make scheduler_id NOT NULL
|
2
|
+
-- First, update any NULL scheduler_id values with generated ones
|
3
|
+
|
4
|
+
-- Update NULL scheduler_id values with generated unique IDs
|
5
|
+
UPDATE scheduled_tasks
|
6
|
+
SET scheduler_id = CONCAT(task_name, '_', task_type, '_', id)
|
7
|
+
WHERE scheduler_id IS NULL;
|
8
|
+
|
9
|
+
-- Now make the column NOT NULL
|
10
|
+
ALTER TABLE scheduled_tasks
|
11
|
+
ALTER COLUMN scheduler_id SET NOT NULL;
|
12
|
+
|
13
|
+
-- Ensure the UNIQUE constraint exists
|
14
|
+
DO $$
|
15
|
+
BEGIN
|
16
|
+
IF NOT EXISTS (
|
17
|
+
SELECT 1
|
18
|
+
FROM pg_constraint
|
19
|
+
WHERE conname = 'scheduled_tasks_scheduler_id_key'
|
20
|
+
) THEN
|
21
|
+
ALTER TABLE scheduled_tasks
|
22
|
+
ADD CONSTRAINT scheduled_tasks_scheduler_id_key UNIQUE (scheduler_id);
|
23
|
+
END IF;
|
24
|
+
END $$;
|
25
|
+
|
26
|
+
-- Update the comment
|
27
|
+
COMMENT ON COLUMN scheduled_tasks.scheduler_id IS
|
28
|
+
'Unique identifier for the task (required, used for deduplication)';
|