jettask 0.2.4__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.
Files changed (94) hide show
  1. jettask/core/cli.py +20 -24
  2. jettask/monitor/run_backlog_collector.py +96 -0
  3. jettask/monitor/stream_backlog_monitor.py +362 -0
  4. jettask/pg_consumer/pg_consumer_v2.py +403 -0
  5. jettask/pg_consumer/sql_utils.py +182 -0
  6. jettask/scheduler/__init__.py +17 -0
  7. jettask/scheduler/add_execution_count.sql +11 -0
  8. jettask/scheduler/add_priority_field.sql +26 -0
  9. jettask/scheduler/add_scheduler_id.sql +25 -0
  10. jettask/scheduler/add_scheduler_id_index.sql +10 -0
  11. jettask/scheduler/loader.py +249 -0
  12. jettask/scheduler/make_scheduler_id_required.sql +28 -0
  13. jettask/scheduler/manager.py +696 -0
  14. jettask/scheduler/migrate_interval_seconds.sql +9 -0
  15. jettask/scheduler/models.py +200 -0
  16. jettask/scheduler/multi_namespace_scheduler.py +294 -0
  17. jettask/scheduler/performance_optimization.sql +45 -0
  18. jettask/scheduler/run_scheduler.py +186 -0
  19. jettask/scheduler/scheduler.py +715 -0
  20. jettask/scheduler/schema.sql +84 -0
  21. jettask/scheduler/unified_manager.py +450 -0
  22. jettask/scheduler/unified_scheduler_manager.py +280 -0
  23. jettask/webui/backend/api/__init__.py +3 -0
  24. jettask/webui/backend/api/v1/__init__.py +17 -0
  25. jettask/webui/backend/api/v1/monitoring.py +431 -0
  26. jettask/webui/backend/api/v1/namespaces.py +504 -0
  27. jettask/webui/backend/api/v1/queues.py +342 -0
  28. jettask/webui/backend/api/v1/tasks.py +367 -0
  29. jettask/webui/backend/core/__init__.py +3 -0
  30. jettask/webui/backend/core/cache.py +221 -0
  31. jettask/webui/backend/core/database.py +200 -0
  32. jettask/webui/backend/core/exceptions.py +102 -0
  33. jettask/webui/backend/models/__init__.py +3 -0
  34. jettask/webui/backend/models/requests.py +236 -0
  35. jettask/webui/backend/models/responses.py +230 -0
  36. jettask/webui/backend/services/__init__.py +3 -0
  37. jettask/webui/frontend/index.html +13 -0
  38. jettask/webui/models/__init__.py +3 -0
  39. jettask/webui/models/namespace.py +63 -0
  40. jettask/webui/sql/batch_upsert_functions.sql +178 -0
  41. jettask/webui/sql/init_database.sql +640 -0
  42. {jettask-0.2.4.dist-info → jettask-0.2.6.dist-info}/METADATA +11 -9
  43. {jettask-0.2.4.dist-info → jettask-0.2.6.dist-info}/RECORD +47 -54
  44. jettask/webui/frontend/package-lock.json +0 -4833
  45. jettask/webui/frontend/package.json +0 -30
  46. jettask/webui/frontend/src/App.css +0 -109
  47. jettask/webui/frontend/src/App.jsx +0 -66
  48. jettask/webui/frontend/src/components/NamespaceSelector.jsx +0 -166
  49. jettask/webui/frontend/src/components/QueueBacklogChart.jsx +0 -298
  50. jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +0 -638
  51. jettask/webui/frontend/src/components/QueueDetailsTable.css +0 -65
  52. jettask/webui/frontend/src/components/QueueDetailsTable.jsx +0 -487
  53. jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +0 -465
  54. jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +0 -423
  55. jettask/webui/frontend/src/components/TaskFilter.jsx +0 -425
  56. jettask/webui/frontend/src/components/TimeRangeSelector.css +0 -21
  57. jettask/webui/frontend/src/components/TimeRangeSelector.jsx +0 -160
  58. jettask/webui/frontend/src/components/charts/QueueChart.jsx +0 -111
  59. jettask/webui/frontend/src/components/charts/QueueTrendChart.jsx +0 -115
  60. jettask/webui/frontend/src/components/charts/WorkerChart.jsx +0 -40
  61. jettask/webui/frontend/src/components/common/StatsCard.jsx +0 -18
  62. jettask/webui/frontend/src/components/layout/AppLayout.css +0 -95
  63. jettask/webui/frontend/src/components/layout/AppLayout.jsx +0 -49
  64. jettask/webui/frontend/src/components/layout/Header.css +0 -106
  65. jettask/webui/frontend/src/components/layout/Header.jsx +0 -106
  66. jettask/webui/frontend/src/components/layout/SideMenu.css +0 -137
  67. jettask/webui/frontend/src/components/layout/SideMenu.jsx +0 -209
  68. jettask/webui/frontend/src/components/layout/TabsNav.css +0 -244
  69. jettask/webui/frontend/src/components/layout/TabsNav.jsx +0 -206
  70. jettask/webui/frontend/src/components/layout/UserInfo.css +0 -197
  71. jettask/webui/frontend/src/components/layout/UserInfo.jsx +0 -197
  72. jettask/webui/frontend/src/contexts/LoadingContext.jsx +0 -27
  73. jettask/webui/frontend/src/contexts/NamespaceContext.jsx +0 -72
  74. jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +0 -245
  75. jettask/webui/frontend/src/index.css +0 -114
  76. jettask/webui/frontend/src/main.jsx +0 -20
  77. jettask/webui/frontend/src/pages/Alerts.jsx +0 -684
  78. jettask/webui/frontend/src/pages/Dashboard/index.css +0 -35
  79. jettask/webui/frontend/src/pages/Dashboard/index.jsx +0 -281
  80. jettask/webui/frontend/src/pages/Dashboard.jsx +0 -1330
  81. jettask/webui/frontend/src/pages/QueueDetail.jsx +0 -1117
  82. jettask/webui/frontend/src/pages/QueueMonitor.jsx +0 -527
  83. jettask/webui/frontend/src/pages/Queues.jsx +0 -12
  84. jettask/webui/frontend/src/pages/ScheduledTasks.jsx +0 -809
  85. jettask/webui/frontend/src/pages/Settings.jsx +0 -800
  86. jettask/webui/frontend/src/pages/Workers.jsx +0 -12
  87. jettask/webui/frontend/src/services/api.js +0 -114
  88. jettask/webui/frontend/src/services/queueTrend.js +0 -152
  89. jettask/webui/frontend/src/utils/suppressWarnings.js +0 -22
  90. jettask/webui/frontend/src/utils/userPreferences.js +0 -154
  91. {jettask-0.2.4.dist-info → jettask-0.2.6.dist-info}/WHEEL +0 -0
  92. {jettask-0.2.4.dist-info → jettask-0.2.6.dist-info}/entry_points.txt +0 -0
  93. {jettask-0.2.4.dist-info → jettask-0.2.6.dist-info}/licenses/LICENSE +0 -0
  94. {jettask-0.2.4.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)';