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.
Files changed (93) hide show
  1. jettask/monitor/run_backlog_collector.py +96 -0
  2. jettask/monitor/stream_backlog_monitor.py +362 -0
  3. jettask/pg_consumer/pg_consumer_v2.py +403 -0
  4. jettask/pg_consumer/sql_utils.py +182 -0
  5. jettask/scheduler/__init__.py +17 -0
  6. jettask/scheduler/add_execution_count.sql +11 -0
  7. jettask/scheduler/add_priority_field.sql +26 -0
  8. jettask/scheduler/add_scheduler_id.sql +25 -0
  9. jettask/scheduler/add_scheduler_id_index.sql +10 -0
  10. jettask/scheduler/loader.py +249 -0
  11. jettask/scheduler/make_scheduler_id_required.sql +28 -0
  12. jettask/scheduler/manager.py +696 -0
  13. jettask/scheduler/migrate_interval_seconds.sql +9 -0
  14. jettask/scheduler/models.py +200 -0
  15. jettask/scheduler/multi_namespace_scheduler.py +294 -0
  16. jettask/scheduler/performance_optimization.sql +45 -0
  17. jettask/scheduler/run_scheduler.py +186 -0
  18. jettask/scheduler/scheduler.py +715 -0
  19. jettask/scheduler/schema.sql +84 -0
  20. jettask/scheduler/unified_manager.py +450 -0
  21. jettask/scheduler/unified_scheduler_manager.py +280 -0
  22. jettask/webui/backend/api/__init__.py +3 -0
  23. jettask/webui/backend/api/v1/__init__.py +17 -0
  24. jettask/webui/backend/api/v1/monitoring.py +431 -0
  25. jettask/webui/backend/api/v1/namespaces.py +504 -0
  26. jettask/webui/backend/api/v1/queues.py +342 -0
  27. jettask/webui/backend/api/v1/tasks.py +367 -0
  28. jettask/webui/backend/core/__init__.py +3 -0
  29. jettask/webui/backend/core/cache.py +221 -0
  30. jettask/webui/backend/core/database.py +200 -0
  31. jettask/webui/backend/core/exceptions.py +102 -0
  32. jettask/webui/backend/models/__init__.py +3 -0
  33. jettask/webui/backend/models/requests.py +236 -0
  34. jettask/webui/backend/models/responses.py +230 -0
  35. jettask/webui/backend/services/__init__.py +3 -0
  36. jettask/webui/frontend/index.html +13 -0
  37. jettask/webui/models/__init__.py +3 -0
  38. jettask/webui/models/namespace.py +63 -0
  39. jettask/webui/sql/batch_upsert_functions.sql +178 -0
  40. jettask/webui/sql/init_database.sql +640 -0
  41. {jettask-0.2.5.dist-info → jettask-0.2.6.dist-info}/METADATA +11 -9
  42. {jettask-0.2.5.dist-info → jettask-0.2.6.dist-info}/RECORD +46 -53
  43. jettask/webui/frontend/package-lock.json +0 -4833
  44. jettask/webui/frontend/package.json +0 -30
  45. jettask/webui/frontend/src/App.css +0 -109
  46. jettask/webui/frontend/src/App.jsx +0 -66
  47. jettask/webui/frontend/src/components/NamespaceSelector.jsx +0 -166
  48. jettask/webui/frontend/src/components/QueueBacklogChart.jsx +0 -298
  49. jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +0 -638
  50. jettask/webui/frontend/src/components/QueueDetailsTable.css +0 -65
  51. jettask/webui/frontend/src/components/QueueDetailsTable.jsx +0 -487
  52. jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +0 -465
  53. jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +0 -423
  54. jettask/webui/frontend/src/components/TaskFilter.jsx +0 -425
  55. jettask/webui/frontend/src/components/TimeRangeSelector.css +0 -21
  56. jettask/webui/frontend/src/components/TimeRangeSelector.jsx +0 -160
  57. jettask/webui/frontend/src/components/charts/QueueChart.jsx +0 -111
  58. jettask/webui/frontend/src/components/charts/QueueTrendChart.jsx +0 -115
  59. jettask/webui/frontend/src/components/charts/WorkerChart.jsx +0 -40
  60. jettask/webui/frontend/src/components/common/StatsCard.jsx +0 -18
  61. jettask/webui/frontend/src/components/layout/AppLayout.css +0 -95
  62. jettask/webui/frontend/src/components/layout/AppLayout.jsx +0 -49
  63. jettask/webui/frontend/src/components/layout/Header.css +0 -106
  64. jettask/webui/frontend/src/components/layout/Header.jsx +0 -106
  65. jettask/webui/frontend/src/components/layout/SideMenu.css +0 -137
  66. jettask/webui/frontend/src/components/layout/SideMenu.jsx +0 -209
  67. jettask/webui/frontend/src/components/layout/TabsNav.css +0 -244
  68. jettask/webui/frontend/src/components/layout/TabsNav.jsx +0 -206
  69. jettask/webui/frontend/src/components/layout/UserInfo.css +0 -197
  70. jettask/webui/frontend/src/components/layout/UserInfo.jsx +0 -197
  71. jettask/webui/frontend/src/contexts/LoadingContext.jsx +0 -27
  72. jettask/webui/frontend/src/contexts/NamespaceContext.jsx +0 -72
  73. jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +0 -245
  74. jettask/webui/frontend/src/index.css +0 -114
  75. jettask/webui/frontend/src/main.jsx +0 -20
  76. jettask/webui/frontend/src/pages/Alerts.jsx +0 -684
  77. jettask/webui/frontend/src/pages/Dashboard/index.css +0 -35
  78. jettask/webui/frontend/src/pages/Dashboard/index.jsx +0 -281
  79. jettask/webui/frontend/src/pages/Dashboard.jsx +0 -1330
  80. jettask/webui/frontend/src/pages/QueueDetail.jsx +0 -1117
  81. jettask/webui/frontend/src/pages/QueueMonitor.jsx +0 -527
  82. jettask/webui/frontend/src/pages/Queues.jsx +0 -12
  83. jettask/webui/frontend/src/pages/ScheduledTasks.jsx +0 -809
  84. jettask/webui/frontend/src/pages/Settings.jsx +0 -800
  85. jettask/webui/frontend/src/pages/Workers.jsx +0 -12
  86. jettask/webui/frontend/src/services/api.js +0 -114
  87. jettask/webui/frontend/src/services/queueTrend.js +0 -152
  88. jettask/webui/frontend/src/utils/suppressWarnings.js +0 -22
  89. jettask/webui/frontend/src/utils/userPreferences.js +0 -154
  90. {jettask-0.2.5.dist-info → jettask-0.2.6.dist-info}/WHEEL +0 -0
  91. {jettask-0.2.5.dist-info → jettask-0.2.6.dist-info}/entry_points.txt +0 -0
  92. {jettask-0.2.5.dist-info → jettask-0.2.6.dist-info}/licenses/LICENSE +0 -0
  93. {jettask-0.2.5.dist-info → jettask-0.2.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,230 @@
1
+ """
2
+ Response models for JetTask WebUI Backend
3
+ """
4
+ from typing import List, Dict, Any, Optional, Generic, TypeVar
5
+ from datetime import datetime
6
+ from pydantic import BaseModel, Field
7
+
8
+ T = TypeVar('T')
9
+
10
+
11
+ class BaseResponse(BaseModel, Generic[T]):
12
+ """基础响应模型"""
13
+ success: bool = Field(default=True, description="请求是否成功")
14
+ message: Optional[str] = Field(default=None, description="响应消息")
15
+ data: Optional[T] = Field(default=None, description="响应数据")
16
+ timestamp: datetime = Field(default_factory=datetime.now, description="响应时间戳")
17
+
18
+
19
+ class PaginatedResponse(BaseResponse[List[T]]):
20
+ """分页响应模型"""
21
+ total: int = Field(description="总记录数")
22
+ page: int = Field(description="当前页码")
23
+ page_size: int = Field(description="每页大小")
24
+ total_pages: int = Field(description="总页数")
25
+
26
+ @classmethod
27
+ def create(
28
+ cls,
29
+ data: List[T],
30
+ total: int,
31
+ page: int,
32
+ page_size: int,
33
+ message: Optional[str] = None
34
+ ):
35
+ """创建分页响应"""
36
+ total_pages = (total + page_size - 1) // page_size
37
+ return cls(
38
+ data=data,
39
+ total=total,
40
+ page=page,
41
+ page_size=page_size,
42
+ total_pages=total_pages,
43
+ message=message
44
+ )
45
+
46
+
47
+ class ErrorResponse(BaseModel):
48
+ """错误响应模型"""
49
+ success: bool = Field(default=False)
50
+ error_code: str = Field(description="错误码")
51
+ message: str = Field(description="错误信息")
52
+ details: Optional[Dict[str, Any]] = Field(default=None, description="错误详情")
53
+ timestamp: datetime = Field(default_factory=datetime.now)
54
+
55
+
56
+ # 队列相关响应模型
57
+ class QueueInfo(BaseModel):
58
+ """队列基本信息"""
59
+ name: str = Field(description="队列名称")
60
+ namespace: str = Field(description="命名空间")
61
+ priority: Optional[int] = Field(default=None, description="优先级")
62
+ pending_count: int = Field(default=0, description="待处理任务数")
63
+ running_count: int = Field(default=0, description="运行中任务数")
64
+ completed_count: int = Field(default=0, description="已完成任务数")
65
+ failed_count: int = Field(default=0, description="失败任务数")
66
+ last_activity: Optional[datetime] = Field(default=None, description="最后活动时间")
67
+
68
+
69
+ class QueueStats(BaseModel):
70
+ """队列统计信息"""
71
+ queue_info: QueueInfo
72
+ consumer_groups: List[Dict[str, Any]] = Field(default_factory=list, description="消费者组信息")
73
+ metrics: Dict[str, Any] = Field(default_factory=dict, description="性能指标")
74
+ trends: Dict[str, Any] = Field(default_factory=dict, description="趋势数据")
75
+
76
+
77
+ class QueueListResponse(BaseResponse[List[QueueInfo]]):
78
+ """队列列表响应"""
79
+ pass
80
+
81
+
82
+ class QueueDetailResponse(BaseResponse[QueueStats]):
83
+ """队列详情响应"""
84
+ pass
85
+
86
+
87
+ # 任务相关响应模型
88
+ class TaskInfo(BaseModel):
89
+ """任务基本信息"""
90
+ id: str = Field(description="任务ID")
91
+ stream_id: Optional[str] = Field(default=None, description="流ID")
92
+ queue: str = Field(description="队列名称")
93
+ task_name: Optional[str] = Field(default=None, description="任务名称")
94
+ status: str = Field(description="任务状态")
95
+ created_at: datetime = Field(description="创建时间")
96
+ started_at: Optional[datetime] = Field(default=None, description="开始时间")
97
+ completed_at: Optional[datetime] = Field(default=None, description="完成时间")
98
+ duration: Optional[float] = Field(default=None, description="执行时长(秒)")
99
+ worker_id: Optional[str] = Field(default=None, description="工作者ID")
100
+ consumer_group: Optional[str] = Field(default=None, description="消费者组")
101
+ priority: Optional[int] = Field(default=None, description="优先级")
102
+
103
+
104
+ class TaskDetail(TaskInfo):
105
+ """任务详细信息"""
106
+ task_data: Optional[Dict[str, Any]] = Field(default=None, description="任务数据")
107
+ result: Optional[Dict[str, Any]] = Field(default=None, description="执行结果")
108
+ error_info: Optional[Dict[str, Any]] = Field(default=None, description="错误信息")
109
+ retry_count: int = Field(default=0, description="重试次数")
110
+ metadata: Optional[Dict[str, Any]] = Field(default=None, description="元数据")
111
+
112
+
113
+ class TaskListResponse(PaginatedResponse[TaskInfo]):
114
+ """任务列表响应"""
115
+ pass
116
+
117
+
118
+ class TaskDetailResponse(BaseResponse[TaskDetail]):
119
+ """任务详情响应"""
120
+ pass
121
+
122
+
123
+ # 监控相关响应模型
124
+ class MetricPoint(BaseModel):
125
+ """指标数据点"""
126
+ timestamp: datetime = Field(description="时间戳")
127
+ value: float = Field(description="指标值")
128
+ metadata: Optional[Dict[str, Any]] = Field(default=None, description="元数据")
129
+
130
+
131
+ class TimeSeries(BaseModel):
132
+ """时间序列数据"""
133
+ name: str = Field(description="序列名称")
134
+ data_points: List[MetricPoint] = Field(description="数据点")
135
+ unit: Optional[str] = Field(default=None, description="单位")
136
+
137
+
138
+ class MonitoringMetrics(BaseModel):
139
+ """监控指标"""
140
+ series: List[TimeSeries] = Field(description="时间序列数据")
141
+ granularity: str = Field(description="数据粒度")
142
+ time_range: Dict[str, datetime] = Field(description="时间范围")
143
+
144
+
145
+ class MonitoringResponse(BaseResponse[MonitoringMetrics]):
146
+ """监控数据响应"""
147
+ pass
148
+
149
+
150
+ # 分析相关响应模型
151
+ class AnalyticsData(BaseModel):
152
+ """分析数据"""
153
+ chart_data: List[Dict[str, Any]] = Field(description="图表数据")
154
+ summary: Dict[str, Any] = Field(description="汇总信息")
155
+ insights: List[str] = Field(default_factory=list, description="分析洞察")
156
+
157
+
158
+ class AnalyticsResponse(BaseResponse[AnalyticsData]):
159
+ """分析响应"""
160
+ pass
161
+
162
+
163
+ # 定时任务相关响应模型
164
+ class ScheduledTaskInfo(BaseModel):
165
+ """定时任务信息"""
166
+ id: str = Field(description="任务ID")
167
+ name: str = Field(description="任务名称")
168
+ namespace: str = Field(description="命名空间")
169
+ queue_name: str = Field(description="队列名称")
170
+ task_name: str = Field(description="任务函数名")
171
+ schedule_type: str = Field(description="调度类型")
172
+ schedule_config: Dict[str, Any] = Field(description="调度配置")
173
+ is_active: bool = Field(description="是否启用")
174
+ created_at: datetime = Field(description="创建时间")
175
+ updated_at: Optional[datetime] = Field(default=None, description="更新时间")
176
+ last_run_at: Optional[datetime] = Field(default=None, description="最后运行时间")
177
+ next_run_at: Optional[datetime] = Field(default=None, description="下次运行时间")
178
+ run_count: int = Field(default=0, description="运行次数")
179
+ success_count: int = Field(default=0, description="成功次数")
180
+ failure_count: int = Field(default=0, description="失败次数")
181
+
182
+
183
+ class ScheduledTaskListResponse(PaginatedResponse[ScheduledTaskInfo]):
184
+ """定时任务列表响应"""
185
+ pass
186
+
187
+
188
+ class ScheduledTaskResponse(BaseResponse[ScheduledTaskInfo]):
189
+ """定时任务响应"""
190
+ pass
191
+
192
+
193
+ # 命名空间相关响应模型
194
+ class NamespaceInfo(BaseModel):
195
+ """命名空间信息"""
196
+ id: str = Field(description="命名空间ID")
197
+ name: str = Field(description="命名空间名称")
198
+ display_name: str = Field(description="显示名称")
199
+ description: Optional[str] = Field(default=None, description="描述")
200
+ redis_url: str = Field(description="Redis连接URL")
201
+ pg_url: Optional[str] = Field(default=None, description="PostgreSQL连接URL")
202
+ created_at: datetime = Field(description="创建时间")
203
+ is_active: bool = Field(default=True, description="是否启用")
204
+ queue_count: int = Field(default=0, description="队列数量")
205
+ task_count: int = Field(default=0, description="任务数量")
206
+
207
+
208
+ class NamespaceListResponse(BaseResponse[List[NamespaceInfo]]):
209
+ """命名空间列表响应"""
210
+ pass
211
+
212
+
213
+ class NamespaceResponse(BaseResponse[NamespaceInfo]):
214
+ """命名空间响应"""
215
+ pass
216
+
217
+
218
+ # 系统状态响应模型
219
+ class SystemHealth(BaseModel):
220
+ """系统健康状态"""
221
+ status: str = Field(description="系统状态")
222
+ version: str = Field(description="系统版本")
223
+ uptime: float = Field(description="运行时间(秒)")
224
+ components: Dict[str, str] = Field(description="组件状态")
225
+ metrics: Dict[str, Any] = Field(description="系统指标")
226
+
227
+
228
+ class HealthResponse(BaseResponse[SystemHealth]):
229
+ """健康检查响应"""
230
+ pass
@@ -0,0 +1,3 @@
1
+ """
2
+ Business logic services for JetTask WebUI Backend
3
+ """
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>JetTask Monitor - 任务监控平台</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,3 @@
1
+ """
2
+ WebUI数据模型
3
+ """
@@ -0,0 +1,63 @@
1
+ """
2
+ 任务中心数据模型
3
+ """
4
+ from dataclasses import dataclass
5
+ from typing import Optional, Dict, Any
6
+ from datetime import datetime
7
+ import uuid
8
+
9
+
10
+ @dataclass
11
+ class Namespace:
12
+ """命名空间模型"""
13
+ id: int # 自增整数ID
14
+ name: str # 唯一的命名空间名称
15
+ connection_url: str # 专属连接URL
16
+ redis_config: Dict[str, Any] # Redis配置
17
+ pg_config: Dict[str, Any] # PostgreSQL配置
18
+ created_at: datetime
19
+ updated_at: datetime
20
+ metadata: Optional[Dict[str, Any]] = None
21
+
22
+ @classmethod
23
+ def create(cls, name: str, redis_config: Dict, pg_config: Dict) -> 'Namespace':
24
+ """创建新的命名空间"""
25
+ # 使用名称作为URL,更直观
26
+ connection_url = f"/api/namespaces/{name}"
27
+ return cls(
28
+ id=0, # 将由数据库自动分配
29
+ name=name,
30
+ connection_url=connection_url,
31
+ redis_config=redis_config,
32
+ pg_config=pg_config,
33
+ created_at=datetime.utcnow(),
34
+ updated_at=datetime.utcnow()
35
+ )
36
+
37
+
38
+ @dataclass
39
+ class TaskCenterConfig:
40
+ """任务中心配置"""
41
+ task_center_url: Optional[str] = None # 任务中心连接URL
42
+ namespace_id: Optional[str] = None # 从URL解析出的命名空间ID
43
+ namespace_name: Optional[str] = None # 命名空间名称
44
+
45
+ @property
46
+ def is_enabled(self) -> bool:
47
+ """是否启用任务中心"""
48
+ return self.task_center_url is not None
49
+
50
+ @classmethod
51
+ def from_url(cls, url: str) -> 'TaskCenterConfig':
52
+ """从连接URL创建配置"""
53
+ if not url or not url.startswith("taskcenter://"):
54
+ return cls()
55
+
56
+ # 解析URL: taskcenter://namespace/{namespace_id}
57
+ parts = url.replace("taskcenter://", "").split("/")
58
+ if len(parts) >= 2 and parts[0] == "namespace":
59
+ return cls(
60
+ task_center_url=url,
61
+ namespace_id=parts[1]
62
+ )
63
+ return cls(task_center_url=url)
@@ -0,0 +1,178 @@
1
+ -- 批量UPSERT函数,用于高效处理分区表的批量插入/更新
2
+
3
+ -- ========================================
4
+ -- 1. 创建临时表类型(用于批量数据传递)
5
+ -- ========================================
6
+
7
+ -- task_runs批量UPSERT函数
8
+ CREATE OR REPLACE FUNCTION batch_upsert_task_runs(
9
+ p_records jsonb
10
+ ) RETURNS INTEGER AS $$
11
+ DECLARE
12
+ v_count INTEGER := 0;
13
+ v_record jsonb;
14
+ BEGIN
15
+ -- 遍历每条记录
16
+ FOR v_record IN SELECT * FROM jsonb_array_elements(p_records)
17
+ LOOP
18
+ -- 先尝试UPDATE
19
+ UPDATE task_runs SET
20
+ consumer_name = COALESCE((v_record->>'consumer_name')::TEXT, consumer_name),
21
+ status = CASE
22
+ WHEN (v_record->>'status')::TEXT IS NULL THEN status
23
+ WHEN status = 'pending' THEN COALESCE((v_record->>'status')::TEXT, status)
24
+ WHEN status = 'running' AND (v_record->>'status')::TEXT IN ('success', 'failed', 'timeout', 'skipped') THEN (v_record->>'status')::TEXT
25
+ WHEN status IN ('success', 'failed', 'timeout', 'skipped') THEN status
26
+ ELSE COALESCE((v_record->>'status')::TEXT, status)
27
+ END,
28
+ result = CASE
29
+ WHEN status IN ('success', 'failed', 'timeout', 'skipped') AND (v_record->>'status')::TEXT NOT IN ('success', 'failed', 'timeout', 'skipped') THEN result
30
+ ELSE COALESCE((v_record->>'result')::jsonb, result)
31
+ END,
32
+ error_message = CASE
33
+ WHEN status IN ('success', 'failed', 'timeout', 'skipped') AND (v_record->>'status')::TEXT NOT IN ('success', 'failed', 'timeout', 'skipped') THEN error_message
34
+ ELSE COALESCE((v_record->>'error_message')::TEXT, error_message)
35
+ END,
36
+ start_time = COALESCE((v_record->>'started_at')::TIMESTAMPTZ, start_time),
37
+ end_time = CASE
38
+ WHEN status IN ('success', 'failed', 'timeout', 'skipped') AND (v_record->>'status')::TEXT NOT IN ('success', 'failed', 'timeout', 'skipped') THEN end_time
39
+ ELSE COALESCE((v_record->>'completed_at')::TIMESTAMPTZ, end_time)
40
+ END,
41
+ worker_id = COALESCE((v_record->>'worker_id')::TEXT, worker_id),
42
+ duration = COALESCE((v_record->>'duration')::DOUBLE PRECISION, duration),
43
+ execution_time = COALESCE((v_record->>'execution_time')::DOUBLE PRECISION, execution_time),
44
+ updated_at = CURRENT_TIMESTAMP
45
+ WHERE stream_id = (v_record->>'stream_id')::TEXT
46
+ AND consumer_group = (v_record->>'consumer_group')::TEXT;
47
+
48
+ -- 如果没有更新到任何行,则INSERT
49
+ IF NOT FOUND THEN
50
+ INSERT INTO task_runs (
51
+ stream_id, task_name, consumer_group, consumer_name, status, result, error_message,
52
+ start_time, end_time, worker_id, duration, execution_time,
53
+ created_at, updated_at
54
+ ) VALUES (
55
+ (v_record->>'stream_id')::TEXT,
56
+ (v_record->>'task_name')::TEXT,
57
+ (v_record->>'consumer_group')::TEXT,
58
+ (v_record->>'consumer_name')::TEXT,
59
+ COALESCE((v_record->>'status')::TEXT, 'pending'),
60
+ (v_record->>'result')::jsonb,
61
+ (v_record->>'error_message')::TEXT,
62
+ (v_record->>'started_at')::TIMESTAMPTZ,
63
+ (v_record->>'completed_at')::TIMESTAMPTZ,
64
+ (v_record->>'worker_id')::TEXT,
65
+ (v_record->>'duration')::DOUBLE PRECISION,
66
+ (v_record->>'execution_time')::DOUBLE PRECISION,
67
+ CURRENT_TIMESTAMP,
68
+ CURRENT_TIMESTAMP
69
+ );
70
+ END IF;
71
+
72
+ v_count := v_count + 1;
73
+ END LOOP;
74
+
75
+ RETURN v_count;
76
+ END;
77
+ $$ LANGUAGE plpgsql;
78
+
79
+ -- ========================================
80
+ -- 2. 批量插入tasks的优化函数
81
+ -- ========================================
82
+
83
+ CREATE OR REPLACE FUNCTION batch_insert_tasks(
84
+ p_records jsonb
85
+ ) RETURNS INTEGER AS $$
86
+ DECLARE
87
+ v_inserted INTEGER;
88
+ BEGIN
89
+ -- 使用单个INSERT语句批量插入,忽略冲突
90
+ WITH data AS (
91
+ SELECT
92
+ (value->>'stream_id')::TEXT as stream_id,
93
+ (value->>'queue')::TEXT as queue,
94
+ (value->>'namespace')::TEXT as namespace,
95
+ (value->>'scheduled_task_id')::TEXT as scheduled_task_id,
96
+ (value->>'payload')::jsonb as payload,
97
+ (value->>'priority')::INTEGER as priority,
98
+ (value->>'created_at')::TIMESTAMPTZ as created_at,
99
+ (value->>'source')::TEXT as source,
100
+ (value->>'metadata')::jsonb as metadata
101
+ FROM jsonb_array_elements(p_records)
102
+ )
103
+ INSERT INTO tasks (stream_id, queue, namespace, scheduled_task_id, payload, priority, created_at, source, metadata)
104
+ SELECT * FROM data
105
+ ON CONFLICT DO NOTHING;
106
+
107
+ GET DIAGNOSTICS v_inserted = ROW_COUNT;
108
+ RETURN v_inserted;
109
+ END;
110
+ $$ LANGUAGE plpgsql;
111
+
112
+ -- ========================================
113
+ -- 3. 创建索引以加速批量操作
114
+ -- ========================================
115
+
116
+ -- 为task_runs的UPSERT操作创建覆盖索引(如果不存在)
117
+ CREATE INDEX IF NOT EXISTS idx_task_runs_upsert
118
+ ON task_runs (stream_id, consumer_group)
119
+ INCLUDE (status, updated_at);
120
+
121
+ -- ========================================
122
+ -- 4. 优化的批量清理函数
123
+ -- ========================================
124
+
125
+ CREATE OR REPLACE FUNCTION cleanup_completed_tasks(
126
+ p_stream_ids TEXT[]
127
+ ) RETURNS INTEGER AS $$
128
+ DECLARE
129
+ v_deleted INTEGER;
130
+ BEGIN
131
+ -- 批量删除已完成的任务
132
+ DELETE FROM task_runs
133
+ WHERE stream_id = ANY(p_stream_ids)
134
+ AND status IN ('success', 'failed', 'timeout', 'skipped', 'cancelled');
135
+
136
+ GET DIAGNOSTICS v_deleted = ROW_COUNT;
137
+ RETURN v_deleted;
138
+ END;
139
+ $$ LANGUAGE plpgsql;
140
+
141
+ -- ========================================
142
+ -- 使用示例
143
+ -- ========================================
144
+
145
+ /*
146
+ -- Python中使用示例:
147
+
148
+ # 批量UPSERT task_runs
149
+ records = [
150
+ {
151
+ 'stream_id': '123-0',
152
+ 'task_name': 'my_task',
153
+ 'consumer_group': 'group1',
154
+ 'status': 'success',
155
+ ...
156
+ },
157
+ ...
158
+ ]
159
+
160
+ query = text("SELECT batch_upsert_task_runs(:records)")
161
+ result = await session.execute(query, {'records': json.dumps(records)})
162
+ count = result.scalar()
163
+
164
+ # 批量插入tasks
165
+ tasks = [
166
+ {
167
+ 'stream_id': '123-0',
168
+ 'queue': 'default',
169
+ 'namespace': 'default',
170
+ ...
171
+ },
172
+ ...
173
+ ]
174
+
175
+ query = text("SELECT batch_insert_tasks(:tasks)")
176
+ result = await session.execute(query, {'tasks': json.dumps(tasks)})
177
+ inserted = result.scalar()
178
+ */