jettask 0.2.18__py3-none-any.whl → 0.2.20__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 (165) hide show
  1. jettask/__init__.py +60 -2
  2. jettask/cli.py +314 -228
  3. jettask/config/__init__.py +9 -1
  4. jettask/config/config.py +245 -0
  5. jettask/config/env_loader.py +381 -0
  6. jettask/config/lua_scripts.py +158 -0
  7. jettask/config/nacos_config.py +132 -5
  8. jettask/core/__init__.py +1 -1
  9. jettask/core/app.py +1573 -666
  10. jettask/core/app_importer.py +33 -16
  11. jettask/core/container.py +532 -0
  12. jettask/core/task.py +1 -4
  13. jettask/core/unified_manager_base.py +2 -2
  14. jettask/executor/__init__.py +38 -0
  15. jettask/executor/core.py +625 -0
  16. jettask/executor/executor.py +338 -0
  17. jettask/executor/orchestrator.py +290 -0
  18. jettask/executor/process_entry.py +638 -0
  19. jettask/executor/task_executor.py +317 -0
  20. jettask/messaging/__init__.py +68 -0
  21. jettask/messaging/event_pool.py +2188 -0
  22. jettask/messaging/reader.py +519 -0
  23. jettask/messaging/registry.py +266 -0
  24. jettask/messaging/scanner.py +369 -0
  25. jettask/messaging/sender.py +312 -0
  26. jettask/persistence/__init__.py +118 -0
  27. jettask/persistence/backlog_monitor.py +567 -0
  28. jettask/{backend/data_access.py → persistence/base.py} +58 -57
  29. jettask/persistence/consumer.py +315 -0
  30. jettask/{core → persistence}/db_manager.py +23 -22
  31. jettask/persistence/maintenance.py +81 -0
  32. jettask/persistence/message_consumer.py +259 -0
  33. jettask/{backend/namespace_data_access.py → persistence/namespace.py} +66 -98
  34. jettask/persistence/offline_recovery.py +196 -0
  35. jettask/persistence/queue_discovery.py +215 -0
  36. jettask/persistence/task_persistence.py +218 -0
  37. jettask/persistence/task_updater.py +583 -0
  38. jettask/scheduler/__init__.py +2 -2
  39. jettask/scheduler/loader.py +6 -5
  40. jettask/scheduler/run_scheduler.py +1 -1
  41. jettask/scheduler/scheduler.py +7 -7
  42. jettask/scheduler/{unified_scheduler_manager.py → scheduler_coordinator.py} +18 -13
  43. jettask/task/__init__.py +16 -0
  44. jettask/{router.py → task/router.py} +26 -8
  45. jettask/task/task_center/__init__.py +9 -0
  46. jettask/task/task_executor.py +318 -0
  47. jettask/task/task_registry.py +291 -0
  48. jettask/test_connection_monitor.py +73 -0
  49. jettask/utils/__init__.py +31 -1
  50. jettask/{monitor/run_backlog_collector.py → utils/backlog_collector.py} +1 -1
  51. jettask/utils/db_connector.py +1629 -0
  52. jettask/{db_init.py → utils/db_init.py} +1 -1
  53. jettask/utils/rate_limit/__init__.py +30 -0
  54. jettask/utils/rate_limit/concurrency_limiter.py +665 -0
  55. jettask/utils/rate_limit/config.py +145 -0
  56. jettask/utils/rate_limit/limiter.py +41 -0
  57. jettask/utils/rate_limit/manager.py +269 -0
  58. jettask/utils/rate_limit/qps_limiter.py +154 -0
  59. jettask/utils/rate_limit/task_limiter.py +384 -0
  60. jettask/utils/serializer.py +3 -0
  61. jettask/{monitor/stream_backlog_monitor.py → utils/stream_backlog.py} +14 -6
  62. jettask/utils/time_sync.py +173 -0
  63. jettask/webui/__init__.py +27 -0
  64. jettask/{api/v1 → webui/api}/alerts.py +1 -1
  65. jettask/{api/v1 → webui/api}/analytics.py +2 -2
  66. jettask/{api/v1 → webui/api}/namespaces.py +1 -1
  67. jettask/{api/v1 → webui/api}/overview.py +1 -1
  68. jettask/{api/v1 → webui/api}/queues.py +3 -3
  69. jettask/{api/v1 → webui/api}/scheduled.py +1 -1
  70. jettask/{api/v1 → webui/api}/settings.py +1 -1
  71. jettask/{api.py → webui/app.py} +253 -145
  72. jettask/webui/namespace_manager/__init__.py +10 -0
  73. jettask/{multi_namespace_consumer.py → webui/namespace_manager/multi.py} +69 -22
  74. jettask/{unified_consumer_manager.py → webui/namespace_manager/unified.py} +1 -1
  75. jettask/{run.py → webui/run.py} +2 -2
  76. jettask/{services → webui/services}/__init__.py +1 -3
  77. jettask/{services → webui/services}/overview_service.py +34 -16
  78. jettask/{services → webui/services}/queue_service.py +1 -1
  79. jettask/{backend → webui/services}/queue_stats_v2.py +1 -1
  80. jettask/{services → webui/services}/settings_service.py +1 -1
  81. jettask/worker/__init__.py +53 -0
  82. jettask/worker/lifecycle.py +1507 -0
  83. jettask/worker/manager.py +583 -0
  84. jettask/{core/offline_worker_recovery.py → worker/recovery.py} +268 -175
  85. {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/METADATA +2 -71
  86. jettask-0.2.20.dist-info/RECORD +145 -0
  87. jettask/__main__.py +0 -140
  88. jettask/api/__init__.py +0 -103
  89. jettask/backend/__init__.py +0 -1
  90. jettask/backend/api/__init__.py +0 -3
  91. jettask/backend/api/v1/__init__.py +0 -17
  92. jettask/backend/api/v1/monitoring.py +0 -431
  93. jettask/backend/api/v1/namespaces.py +0 -504
  94. jettask/backend/api/v1/queues.py +0 -342
  95. jettask/backend/api/v1/tasks.py +0 -367
  96. jettask/backend/core/__init__.py +0 -3
  97. jettask/backend/core/cache.py +0 -221
  98. jettask/backend/core/database.py +0 -200
  99. jettask/backend/core/exceptions.py +0 -102
  100. jettask/backend/dependencies.py +0 -261
  101. jettask/backend/init_meta_db.py +0 -158
  102. jettask/backend/main.py +0 -1426
  103. jettask/backend/main_unified.py +0 -78
  104. jettask/backend/main_v2.py +0 -394
  105. jettask/backend/models/__init__.py +0 -3
  106. jettask/backend/models/requests.py +0 -236
  107. jettask/backend/models/responses.py +0 -230
  108. jettask/backend/namespace_api_old.py +0 -267
  109. jettask/backend/services/__init__.py +0 -3
  110. jettask/backend/start.py +0 -42
  111. jettask/backend/unified_api_router.py +0 -1541
  112. jettask/cleanup_deprecated_tables.sql +0 -16
  113. jettask/core/consumer_manager.py +0 -1695
  114. jettask/core/delay_scanner.py +0 -256
  115. jettask/core/event_pool.py +0 -1700
  116. jettask/core/heartbeat_process.py +0 -222
  117. jettask/core/task_batch.py +0 -153
  118. jettask/core/worker_scanner.py +0 -271
  119. jettask/executors/__init__.py +0 -5
  120. jettask/executors/asyncio.py +0 -876
  121. jettask/executors/base.py +0 -30
  122. jettask/executors/common.py +0 -148
  123. jettask/executors/multi_asyncio.py +0 -309
  124. jettask/gradio_app.py +0 -570
  125. jettask/integrated_gradio_app.py +0 -1088
  126. jettask/main.py +0 -0
  127. jettask/monitoring/__init__.py +0 -3
  128. jettask/pg_consumer.py +0 -1896
  129. jettask/run_monitor.py +0 -22
  130. jettask/run_webui.py +0 -148
  131. jettask/scheduler/multi_namespace_scheduler.py +0 -294
  132. jettask/scheduler/unified_manager.py +0 -450
  133. jettask/task_center_client.py +0 -150
  134. jettask/utils/serializer_optimized.py +0 -33
  135. jettask/webui_exceptions.py +0 -67
  136. jettask-0.2.18.dist-info/RECORD +0 -150
  137. /jettask/{constants.py → config/constants.py} +0 -0
  138. /jettask/{backend/config.py → config/task_center.py} +0 -0
  139. /jettask/{pg_consumer → messaging/pg_consumer}/pg_consumer_v2.py +0 -0
  140. /jettask/{pg_consumer → messaging/pg_consumer}/sql/add_execution_time_field.sql +0 -0
  141. /jettask/{pg_consumer → messaging/pg_consumer}/sql/create_new_tables.sql +0 -0
  142. /jettask/{pg_consumer → messaging/pg_consumer}/sql/create_tables_v3.sql +0 -0
  143. /jettask/{pg_consumer → messaging/pg_consumer}/sql/migrate_to_new_structure.sql +0 -0
  144. /jettask/{pg_consumer → messaging/pg_consumer}/sql/modify_time_fields.sql +0 -0
  145. /jettask/{pg_consumer → messaging/pg_consumer}/sql_utils.py +0 -0
  146. /jettask/{models.py → persistence/models.py} +0 -0
  147. /jettask/scheduler/{manager.py → task_crud.py} +0 -0
  148. /jettask/{schema.sql → schemas/schema.sql} +0 -0
  149. /jettask/{task_center.py → task/task_center/client.py} +0 -0
  150. /jettask/{monitoring → utils}/file_watcher.py +0 -0
  151. /jettask/{services/redis_monitor_service.py → utils/redis_monitor.py} +0 -0
  152. /jettask/{api/v1 → webui/api}/__init__.py +0 -0
  153. /jettask/{webui_config.py → webui/config.py} +0 -0
  154. /jettask/{webui_models → webui/models}/__init__.py +0 -0
  155. /jettask/{webui_models → webui/models}/namespace.py +0 -0
  156. /jettask/{services → webui/services}/alert_service.py +0 -0
  157. /jettask/{services → webui/services}/analytics_service.py +0 -0
  158. /jettask/{services → webui/services}/scheduled_task_service.py +0 -0
  159. /jettask/{services → webui/services}/task_service.py +0 -0
  160. /jettask/{webui_sql → webui/sql}/batch_upsert_functions.sql +0 -0
  161. /jettask/{webui_sql → webui/sql}/verify_database.sql +0 -0
  162. {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/WHEEL +0 -0
  163. {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/entry_points.txt +0 -0
  164. {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/licenses/LICENSE +0 -0
  165. {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/top_level.txt +0 -0
@@ -1,367 +0,0 @@
1
- """
2
- Task management API v1
3
- """
4
- from typing import List, Optional
5
- from fastapi import APIRouter, Depends, HTTPException, Query, Body
6
- from sqlalchemy.ext.asyncio import AsyncSession
7
- import redis.asyncio as redis
8
-
9
- from dependencies import (
10
- get_validated_namespace, get_pg_connection, get_redis_client,
11
- validate_page_params, validate_time_range, get_request_metrics, RequestMetrics
12
- )
13
- from models.requests import TaskListRequest, TaskActionRequest
14
- from models.responses import TaskListResponse, TaskDetailResponse, BaseResponse
15
- from core.cache import cache_result, CACHE_CONFIGS
16
- from core.exceptions import TaskNotFoundError, ValidationError
17
- from data_access import JetTaskDataAccess
18
- import logging
19
-
20
- logger = logging.getLogger(__name__)
21
- router = APIRouter()
22
-
23
-
24
- @router.post("/search", response_model=TaskListResponse)
25
- async def search_tasks(
26
- request: TaskListRequest,
27
- namespace: str = Depends(get_validated_namespace),
28
- pg_session: AsyncSession = Depends(get_pg_connection),
29
- metrics: RequestMetrics = Depends(get_request_metrics)
30
- ):
31
- """搜索任务(支持复杂筛选条件)"""
32
- metrics.start(namespace, "POST /tasks/search")
33
-
34
- try:
35
- # 创建数据访问实例
36
- data_access = JetTaskDataAccess()
37
-
38
- # 转换筛选条件
39
- filters = []
40
- for condition in request.filters:
41
- filters.append({
42
- 'field': condition.field,
43
- 'operator': condition.operator.value,
44
- 'value': condition.value
45
- })
46
-
47
- # 添加基础筛选条件
48
- if request.queue_name:
49
- filters.append({'field': 'queue', 'operator': 'eq', 'value': request.queue_name})
50
- if request.status:
51
- filters.append({'field': 'status', 'operator': 'eq', 'value': request.status})
52
- if request.consumer_group:
53
- filters.append({'field': 'consumer_group', 'operator': 'eq', 'value': request.consumer_group})
54
- if request.worker_id:
55
- filters.append({'field': 'worker_id', 'operator': 'eq', 'value': request.worker_id})
56
- if request.task_name:
57
- filters.append({'field': 'task_name', 'operator': 'like', 'value': f"%{request.task_name}%"})
58
-
59
- # 执行查询
60
- result = await data_access.fetch_tasks_with_filters(
61
- queue_name=request.queue_name or "",
62
- page=request.page,
63
- page_size=request.page_size,
64
- filters=filters,
65
- start_time=request.start_time,
66
- end_time=request.end_time,
67
- sort_field=request.sort_field,
68
- sort_order=request.sort_order.value if request.sort_order else 'desc',
69
- search=request.search
70
- )
71
-
72
- return TaskListResponse.create(
73
- data=result['data'],
74
- total=result['total'],
75
- page=request.page,
76
- page_size=request.page_size
77
- )
78
-
79
- except Exception as e:
80
- logger.error(f"搜索任务失败: {e}")
81
- raise HTTPException(status_code=500, detail=str(e))
82
- finally:
83
- metrics.finish()
84
-
85
-
86
- @router.get("", response_model=TaskListResponse)
87
- @cache_result(**CACHE_CONFIGS['task_details'])
88
- async def list_tasks(
89
- namespace: str = Depends(get_validated_namespace),
90
- queue_name: Optional[str] = Query(None, description="队列名称"),
91
- status: Optional[str] = Query(None, description="任务状态"),
92
- consumer_group: Optional[str] = Query(None, description="消费者组"),
93
- worker_id: Optional[str] = Query(None, description="工作者ID"),
94
- task_name: Optional[str] = Query(None, description="任务名称"),
95
- page: int = Query(1, ge=1, description="页码"),
96
- page_size: int = Query(20, ge=1, le=100, description="每页大小"),
97
- sort_field: Optional[str] = Query("created_at", description="排序字段"),
98
- sort_order: str = Query("desc", pattern="^(asc|desc)$", description="排序方向"),
99
- search: Optional[str] = Query(None, description="搜索关键词"),
100
- time_params: dict = Depends(validate_time_range),
101
- pg_session: AsyncSession = Depends(get_pg_connection),
102
- metrics: RequestMetrics = Depends(get_request_metrics)
103
- ):
104
- """获取任务列表(简单查询)"""
105
- metrics.start(namespace, "GET /tasks")
106
-
107
- try:
108
- # 创建数据访问实例
109
- data_access = JetTaskDataAccess()
110
-
111
- # 构建筛选条件
112
- filters = []
113
- if status:
114
- filters.append({'field': 'status', 'operator': 'eq', 'value': status})
115
- if consumer_group:
116
- filters.append({'field': 'consumer_group', 'operator': 'eq', 'value': consumer_group})
117
- if worker_id:
118
- filters.append({'field': 'worker_id', 'operator': 'eq', 'value': worker_id})
119
- if task_name:
120
- filters.append({'field': 'task_name', 'operator': 'like', 'value': f"%{task_name}%"})
121
-
122
- # 执行查询
123
- result = await data_access.fetch_tasks_with_filters(
124
- queue_name=queue_name or "",
125
- page=page,
126
- page_size=page_size,
127
- filters=filters,
128
- start_time=time_params.get('start_time'),
129
- end_time=time_params.get('end_time'),
130
- sort_field=sort_field,
131
- sort_order=sort_order,
132
- search=search
133
- )
134
-
135
- return TaskListResponse.create(
136
- data=result['data'],
137
- total=result['total'],
138
- page=page,
139
- page_size=page_size
140
- )
141
-
142
- except Exception as e:
143
- logger.error(f"获取任务列表失败: {e}")
144
- raise HTTPException(status_code=500, detail=str(e))
145
- finally:
146
- metrics.finish()
147
-
148
-
149
- @router.get("/{task_id}", response_model=TaskDetailResponse)
150
- @cache_result(**CACHE_CONFIGS['task_details'])
151
- async def get_task_detail(
152
- task_id: str,
153
- namespace: str = Depends(get_validated_namespace),
154
- consumer_group: Optional[str] = Query(None, description="消费者组"),
155
- pg_session: AsyncSession = Depends(get_pg_connection),
156
- metrics: RequestMetrics = Depends(get_request_metrics)
157
- ):
158
- """获取任务详情"""
159
- metrics.start(namespace, f"GET /tasks/{task_id}")
160
-
161
- try:
162
- # 创建数据访问实例
163
- data_access = JetTaskDataAccess()
164
-
165
- # 获取任务详情
166
- task_detail = await data_access.fetch_task_details(task_id, consumer_group)
167
-
168
- if not task_detail:
169
- raise TaskNotFoundError(task_id)
170
-
171
- return TaskDetailResponse(data=task_detail)
172
-
173
- except TaskNotFoundError:
174
- raise
175
- except Exception as e:
176
- logger.error(f"获取任务详情失败: {e}")
177
- raise HTTPException(status_code=500, detail=str(e))
178
- finally:
179
- metrics.finish()
180
-
181
-
182
- @router.post("/actions", response_model=BaseResponse)
183
- async def execute_task_actions(
184
- action_request: TaskActionRequest,
185
- namespace: str = Depends(get_validated_namespace),
186
- redis_client: redis.Redis = Depends(get_redis_client),
187
- pg_session: AsyncSession = Depends(get_pg_connection),
188
- metrics: RequestMetrics = Depends(get_request_metrics)
189
- ):
190
- """批量执行任务操作"""
191
- metrics.start(namespace, "POST /tasks/actions")
192
-
193
- try:
194
- action = action_request.action.lower()
195
- task_ids = action_request.task_ids
196
- parameters = action_request.parameters
197
-
198
- if not task_ids:
199
- raise ValidationError("task_ids cannot be empty")
200
-
201
- # 验证任务ID格式
202
- for task_id in task_ids:
203
- if not task_id or not isinstance(task_id, str):
204
- raise ValidationError(f"Invalid task_id: {task_id}")
205
-
206
- success_count = 0
207
- failed_count = 0
208
- errors = []
209
-
210
- if action == "retry":
211
- # 重试失败任务
212
- for task_id in task_ids:
213
- try:
214
- # 这里需要实现重试逻辑
215
- # 1. 检查任务状态是否为failed
216
- # 2. 重新入队任务
217
- # 3. 更新任务状态
218
- success_count += 1
219
- except Exception as e:
220
- failed_count += 1
221
- errors.append(f"Task {task_id}: {str(e)}")
222
-
223
- elif action == "cancel":
224
- # 取消待处理任务
225
- for task_id in task_ids:
226
- try:
227
- # 这里需要实现取消逻辑
228
- # 1. 检查任务状态
229
- # 2. 从队列中移除
230
- # 3. 更新任务状态为cancelled
231
- success_count += 1
232
- except Exception as e:
233
- failed_count += 1
234
- errors.append(f"Task {task_id}: {str(e)}")
235
-
236
- elif action == "delete":
237
- # 删除任务记录
238
- for task_id in task_ids:
239
- try:
240
- # 这里需要实现删除逻辑
241
- # 1. 从数据库删除任务记录
242
- # 2. 清理相关数据
243
- success_count += 1
244
- except Exception as e:
245
- failed_count += 1
246
- errors.append(f"Task {task_id}: {str(e)}")
247
-
248
- else:
249
- raise ValidationError(f"Unsupported action: {action}")
250
-
251
- # 构造响应消息
252
- message = f"Action '{action}' executed: {success_count} succeeded"
253
- if failed_count > 0:
254
- message += f", {failed_count} failed"
255
-
256
- response_data = {
257
- 'action': action,
258
- 'total_tasks': len(task_ids),
259
- 'success_count': success_count,
260
- 'failed_count': failed_count,
261
- 'errors': errors[:10] # 只返回前10个错误
262
- }
263
-
264
- return BaseResponse(
265
- message=message,
266
- data=response_data
267
- )
268
-
269
- except ValidationError:
270
- raise
271
- except Exception as e:
272
- logger.error(f"执行任务操作失败: {e}")
273
- raise HTTPException(status_code=500, detail=str(e))
274
- finally:
275
- metrics.finish()
276
-
277
-
278
- @router.get("/{task_id}/logs")
279
- async def get_task_logs(
280
- task_id: str,
281
- namespace: str = Depends(get_validated_namespace),
282
- lines: int = Query(100, ge=1, le=1000, description="日志行数"),
283
- follow: bool = Query(False, description="是否跟踪日志"),
284
- pg_session: AsyncSession = Depends(get_pg_connection),
285
- metrics: RequestMetrics = Depends(get_request_metrics)
286
- ):
287
- """获取任务执行日志"""
288
- metrics.start(namespace, f"GET /tasks/{task_id}/logs")
289
-
290
- try:
291
- # 这里需要实现日志获取逻辑
292
- # 1. 从数据库或日志系统获取任务执行日志
293
- # 2. 支持实时日志跟踪(WebSocket)
294
-
295
- # 暂时返回模拟数据
296
- logs = [
297
- {"timestamp": "2025-09-08T12:00:00Z", "level": "INFO", "message": "Task started"},
298
- {"timestamp": "2025-09-08T12:00:01Z", "level": "DEBUG", "message": "Processing data..."},
299
- {"timestamp": "2025-09-08T12:00:02Z", "level": "INFO", "message": "Task completed successfully"}
300
- ]
301
-
302
- return BaseResponse(
303
- data={
304
- "task_id": task_id,
305
- "logs": logs[-lines:],
306
- "total_lines": len(logs),
307
- "truncated": len(logs) > lines
308
- }
309
- )
310
-
311
- except Exception as e:
312
- logger.error(f"获取任务日志失败: {e}")
313
- raise HTTPException(status_code=500, detail=str(e))
314
- finally:
315
- metrics.finish()
316
-
317
-
318
- @router.get("/statistics/summary")
319
- async def get_task_statistics(
320
- namespace: str = Depends(get_validated_namespace),
321
- queue_name: Optional[str] = Query(None, description="队列名称"),
322
- time_params: dict = Depends(validate_time_range),
323
- pg_session: AsyncSession = Depends(get_pg_connection),
324
- metrics: RequestMetrics = Depends(get_request_metrics)
325
- ):
326
- """获取任务统计摘要"""
327
- metrics.start(namespace, "GET /tasks/statistics/summary")
328
-
329
- try:
330
- # 这里需要实现统计查询逻辑
331
- # 1. 按状态统计任务数量
332
- # 2. 按时间统计任务趋势
333
- # 3. 按队列统计任务分布
334
- # 4. 计算成功率、平均执行时间等指标
335
-
336
- # 暂时返回模拟数据
337
- statistics = {
338
- "total_tasks": 12345,
339
- "by_status": {
340
- "pending": 123,
341
- "running": 45,
342
- "completed": 11000,
343
- "failed": 1177
344
- },
345
- "by_queue": {
346
- "shared_queue": 8000,
347
- "priority_queue": 3000,
348
- "background_queue": 1345
349
- },
350
- "metrics": {
351
- "success_rate": 0.905,
352
- "avg_execution_time": 12.34,
353
- "tasks_per_hour": 234.5
354
- },
355
- "trends": {
356
- "hourly_counts": [],
357
- "error_rates": []
358
- }
359
- }
360
-
361
- return BaseResponse(data=statistics)
362
-
363
- except Exception as e:
364
- logger.error(f"获取任务统计失败: {e}")
365
- raise HTTPException(status_code=500, detail=str(e))
366
- finally:
367
- metrics.finish()
@@ -1,3 +0,0 @@
1
- """
2
- Core infrastructure modules for JetTask WebUI Backend
3
- """
@@ -1,221 +0,0 @@
1
- """
2
- Caching utilities for JetTask WebUI Backend
3
- """
4
- import json
5
- import hashlib
6
- from typing import Any, Optional, Dict, Callable, TypeVar, Union
7
- from functools import wraps
8
- import asyncio
9
- import time
10
- import logging
11
-
12
- import redis.asyncio as redis
13
-
14
- logger = logging.getLogger(__name__)
15
-
16
- T = TypeVar('T')
17
-
18
-
19
- class CacheManager:
20
- """缓存管理器"""
21
-
22
- def __init__(self, redis_client: Optional[redis.Redis] = None):
23
- self.redis_client = redis_client
24
- self.local_cache: Dict[str, Dict[str, Any]] = {}
25
- self.cache_stats = {
26
- 'hits': 0,
27
- 'misses': 0,
28
- 'sets': 0,
29
- 'deletes': 0
30
- }
31
-
32
- def _generate_key(self, prefix: str, *args, **kwargs) -> str:
33
- """生成缓存键"""
34
- key_data = f"{prefix}:{args}:{sorted(kwargs.items())}"
35
- return f"jettask:cache:{hashlib.md5(key_data.encode()).hexdigest()}"
36
-
37
- async def get(self, key: str) -> Optional[Any]:
38
- """获取缓存值"""
39
- try:
40
- # 先检查本地缓存
41
- if key in self.local_cache:
42
- local_data = self.local_cache[key]
43
- if local_data['expires'] > time.time():
44
- self.cache_stats['hits'] += 1
45
- return local_data['value']
46
- else:
47
- del self.local_cache[key]
48
-
49
- # 检查Redis缓存
50
- if self.redis_client:
51
- value = await self.redis_client.get(key)
52
- if value:
53
- self.cache_stats['hits'] += 1
54
- return json.loads(value)
55
-
56
- self.cache_stats['misses'] += 1
57
- return None
58
-
59
- except Exception as e:
60
- logger.warning(f"缓存获取失败: {e}")
61
- self.cache_stats['misses'] += 1
62
- return None
63
-
64
- async def set(
65
- self,
66
- key: str,
67
- value: Any,
68
- ttl: int = 300,
69
- local_only: bool = False
70
- ) -> bool:
71
- """设置缓存值"""
72
- try:
73
- # 设置本地缓存
74
- if ttl <= 60 or local_only: # 短期缓存或仅本地缓存
75
- self.local_cache[key] = {
76
- 'value': value,
77
- 'expires': time.time() + ttl
78
- }
79
-
80
- # 设置Redis缓存
81
- if self.redis_client and not local_only:
82
- await self.redis_client.setex(
83
- key,
84
- ttl,
85
- json.dumps(value, default=str)
86
- )
87
-
88
- self.cache_stats['sets'] += 1
89
- return True
90
-
91
- except Exception as e:
92
- logger.warning(f"缓存设置失败: {e}")
93
- return False
94
-
95
- async def delete(self, key: str) -> bool:
96
- """删除缓存"""
97
- try:
98
- # 删除本地缓存
99
- if key in self.local_cache:
100
- del self.local_cache[key]
101
-
102
- # 删除Redis缓存
103
- if self.redis_client:
104
- await self.redis_client.delete(key)
105
-
106
- self.cache_stats['deletes'] += 1
107
- return True
108
-
109
- except Exception as e:
110
- logger.warning(f"缓存删除失败: {e}")
111
- return False
112
-
113
- async def clear_pattern(self, pattern: str) -> int:
114
- """根据模式删除缓存"""
115
- try:
116
- deleted = 0
117
-
118
- # 清理本地缓存
119
- keys_to_delete = [k for k in self.local_cache.keys() if pattern in k]
120
- for key in keys_to_delete:
121
- del self.local_cache[key]
122
- deleted += 1
123
-
124
- # 清理Redis缓存
125
- if self.redis_client:
126
- keys = await self.redis_client.keys(f"*{pattern}*")
127
- if keys:
128
- await self.redis_client.delete(*keys)
129
- deleted += len(keys)
130
-
131
- self.cache_stats['deletes'] += deleted
132
- return deleted
133
-
134
- except Exception as e:
135
- logger.warning(f"批量删除缓存失败: {e}")
136
- return 0
137
-
138
- def cleanup_expired(self):
139
- """清理过期的本地缓存"""
140
- current_time = time.time()
141
- expired_keys = [
142
- key for key, data in self.local_cache.items()
143
- if data['expires'] <= current_time
144
- ]
145
- for key in expired_keys:
146
- del self.local_cache[key]
147
-
148
- def get_stats(self) -> Dict[str, Any]:
149
- """获取缓存统计信息"""
150
- hit_rate = (
151
- self.cache_stats['hits'] /
152
- (self.cache_stats['hits'] + self.cache_stats['misses'])
153
- if (self.cache_stats['hits'] + self.cache_stats['misses']) > 0
154
- else 0
155
- )
156
-
157
- return {
158
- **self.cache_stats,
159
- 'hit_rate': hit_rate,
160
- 'local_cache_size': len(self.local_cache)
161
- }
162
-
163
-
164
- # 全局缓存管理器
165
- cache_manager = CacheManager()
166
-
167
-
168
- def cache_result(
169
- ttl: int = 300,
170
- key_prefix: str = "default",
171
- local_only: bool = False
172
- ):
173
- """缓存装饰器"""
174
- def decorator(func: Callable[..., T]) -> Callable[..., T]:
175
- @wraps(func)
176
- async def wrapper(*args, **kwargs) -> T:
177
- # 生成缓存键
178
- cache_key = cache_manager._generate_key(
179
- f"{key_prefix}:{func.__name__}",
180
- *args,
181
- **kwargs
182
- )
183
-
184
- # 尝试从缓存获取
185
- cached_result = await cache_manager.get(cache_key)
186
- if cached_result is not None:
187
- return cached_result
188
-
189
- # 执行函数
190
- result = await func(*args, **kwargs)
191
-
192
- # 缓存结果
193
- await cache_manager.set(cache_key, result, ttl, local_only)
194
-
195
- return result
196
-
197
- return wrapper
198
- return decorator
199
-
200
-
201
- def invalidate_cache(pattern: str):
202
- """缓存失效装饰器"""
203
- def decorator(func: Callable[..., T]) -> Callable[..., T]:
204
- @wraps(func)
205
- async def wrapper(*args, **kwargs) -> T:
206
- result = await func(*args, **kwargs)
207
- # 执行后清理相关缓存
208
- await cache_manager.clear_pattern(pattern)
209
- return result
210
-
211
- return wrapper
212
- return decorator
213
-
214
-
215
- # 预定义的缓存配置
216
- CACHE_CONFIGS = {
217
- 'queue_stats': {'ttl': 300, 'key_prefix': 'queue_stats'}, # 5分钟
218
- 'task_details': {'ttl': 60, 'key_prefix': 'task_details'}, # 1分钟
219
- 'namespace_config': {'ttl': 1800, 'key_prefix': 'namespace_config'}, # 30分钟
220
- 'monitoring_data': {'ttl': 30, 'key_prefix': 'monitoring_data', 'local_only': True}, # 30秒,仅本地
221
- }