django-cfg 1.4.120__py3-none-any.whl → 1.5.2__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.
Potentially problematic release.
This version of django-cfg might be problematic. Click here for more details.
- django_cfg/__init__.py +8 -4
- django_cfg/apps/centrifugo/admin/centrifugo_log.py +33 -71
- django_cfg/apps/dashboard/TRANSACTION_FIX.md +73 -0
- django_cfg/apps/dashboard/serializers/__init__.py +0 -12
- django_cfg/apps/dashboard/serializers/activity.py +1 -1
- django_cfg/apps/dashboard/services/__init__.py +0 -2
- django_cfg/apps/dashboard/services/charts_service.py +4 -3
- django_cfg/apps/dashboard/services/statistics_service.py +11 -2
- django_cfg/apps/dashboard/services/system_health_service.py +64 -106
- django_cfg/apps/dashboard/urls.py +0 -2
- django_cfg/apps/dashboard/views/__init__.py +0 -2
- django_cfg/apps/dashboard/views/commands_views.py +3 -6
- django_cfg/apps/dashboard/views/overview_views.py +14 -13
- django_cfg/apps/grpc/__init__.py +9 -0
- django_cfg/apps/grpc/admin/__init__.py +11 -0
- django_cfg/apps/{tasks → grpc}/admin/config.py +32 -41
- django_cfg/apps/grpc/admin/grpc_request_log.py +252 -0
- django_cfg/apps/grpc/apps.py +28 -0
- django_cfg/apps/grpc/auth/__init__.py +9 -0
- django_cfg/apps/grpc/auth/jwt_auth.py +295 -0
- django_cfg/apps/grpc/interceptors/__init__.py +19 -0
- django_cfg/apps/grpc/interceptors/errors.py +241 -0
- django_cfg/apps/grpc/interceptors/logging.py +270 -0
- django_cfg/apps/grpc/interceptors/metrics.py +306 -0
- django_cfg/apps/grpc/interceptors/request_logger.py +515 -0
- django_cfg/apps/grpc/management/__init__.py +1 -0
- django_cfg/apps/grpc/management/commands/rungrpc.py +302 -0
- django_cfg/apps/grpc/managers/__init__.py +10 -0
- django_cfg/apps/grpc/managers/grpc_request_log.py +310 -0
- django_cfg/apps/grpc/migrations/0001_initial.py +69 -0
- django_cfg/apps/grpc/migrations/0002_rename_django_cfg__service_4c4a8e_idx_django_cfg__service_584308_idx_and_more.py +38 -0
- django_cfg/apps/grpc/models/__init__.py +9 -0
- django_cfg/apps/grpc/models/grpc_request_log.py +219 -0
- django_cfg/apps/grpc/serializers/__init__.py +23 -0
- django_cfg/apps/grpc/serializers/health.py +18 -0
- django_cfg/apps/grpc/serializers/requests.py +18 -0
- django_cfg/apps/grpc/serializers/services.py +50 -0
- django_cfg/apps/grpc/serializers/stats.py +22 -0
- django_cfg/apps/grpc/services/__init__.py +16 -0
- django_cfg/apps/grpc/services/base.py +375 -0
- django_cfg/apps/grpc/services/discovery.py +415 -0
- django_cfg/apps/grpc/urls.py +23 -0
- django_cfg/apps/grpc/utils/__init__.py +13 -0
- django_cfg/apps/grpc/utils/proto_gen.py +423 -0
- django_cfg/apps/grpc/views/__init__.py +9 -0
- django_cfg/apps/grpc/views/monitoring.py +497 -0
- django_cfg/apps/knowbase/apps.py +2 -2
- django_cfg/apps/maintenance/admin/api_key_admin.py +7 -9
- django_cfg/apps/maintenance/admin/site_admin.py +5 -4
- django_cfg/apps/newsletter/admin/newsletter_admin.py +12 -11
- django_cfg/apps/payments/admin/balance_admin.py +26 -36
- django_cfg/apps/payments/admin/payment_admin.py +65 -85
- django_cfg/apps/payments/admin/withdrawal_admin.py +65 -100
- django_cfg/apps/rq/__init__.py +9 -0
- django_cfg/apps/rq/apps.py +80 -0
- django_cfg/apps/rq/management/__init__.py +1 -0
- django_cfg/apps/rq/management/commands/__init__.py +1 -0
- django_cfg/apps/rq/management/commands/rqscheduler.py +31 -0
- django_cfg/apps/rq/management/commands/rqstats.py +33 -0
- django_cfg/apps/rq/management/commands/rqworker.py +31 -0
- django_cfg/apps/rq/management/commands/rqworker_pool.py +27 -0
- django_cfg/apps/rq/serializers/__init__.py +40 -0
- django_cfg/apps/rq/serializers/health.py +60 -0
- django_cfg/apps/rq/serializers/job.py +100 -0
- django_cfg/apps/rq/serializers/queue.py +80 -0
- django_cfg/apps/rq/serializers/schedule.py +178 -0
- django_cfg/apps/rq/serializers/testing.py +139 -0
- django_cfg/apps/rq/serializers/worker.py +58 -0
- django_cfg/apps/rq/services/__init__.py +25 -0
- django_cfg/apps/rq/services/config_helper.py +233 -0
- django_cfg/apps/rq/services/models/README.md +417 -0
- django_cfg/apps/rq/services/models/__init__.py +30 -0
- django_cfg/apps/rq/services/models/event.py +123 -0
- django_cfg/apps/rq/services/models/job.py +99 -0
- django_cfg/apps/rq/services/models/queue.py +92 -0
- django_cfg/apps/rq/services/models/worker.py +104 -0
- django_cfg/apps/rq/services/rq_converters.py +183 -0
- django_cfg/apps/rq/tasks/__init__.py +23 -0
- django_cfg/apps/rq/tasks/demo_tasks.py +284 -0
- django_cfg/apps/rq/urls.py +54 -0
- django_cfg/apps/rq/views/__init__.py +19 -0
- django_cfg/apps/rq/views/jobs.py +882 -0
- django_cfg/apps/rq/views/monitoring.py +248 -0
- django_cfg/apps/rq/views/queues.py +261 -0
- django_cfg/apps/rq/views/schedule.py +400 -0
- django_cfg/apps/rq/views/testing.py +761 -0
- django_cfg/apps/rq/views/workers.py +195 -0
- django_cfg/apps/urls.py +13 -8
- django_cfg/config.py +106 -0
- django_cfg/core/base/config_model.py +16 -26
- django_cfg/core/builders/apps_builder.py +7 -11
- django_cfg/core/generation/integration_generators/__init__.py +3 -6
- django_cfg/core/generation/integration_generators/django_rq.py +80 -0
- django_cfg/core/generation/integration_generators/grpc_generator.py +318 -0
- django_cfg/core/generation/orchestrator.py +15 -15
- django_cfg/core/integration/display/startup.py +6 -20
- django_cfg/mixins/__init__.py +2 -0
- django_cfg/mixins/superadmin_api.py +59 -0
- django_cfg/models/__init__.py +3 -3
- django_cfg/models/api/grpc/__init__.py +59 -0
- django_cfg/models/api/grpc/config.py +364 -0
- django_cfg/models/django/__init__.py +3 -3
- django_cfg/models/django/django_rq.py +621 -0
- django_cfg/models/django/revolution_legacy.py +1 -1
- django_cfg/modules/base.py +19 -6
- django_cfg/modules/django_admin/base/pydantic_admin.py +2 -2
- django_cfg/modules/django_admin/config/background_task_config.py +4 -4
- django_cfg/modules/django_admin/utils/__init__.py +41 -3
- django_cfg/modules/django_admin/utils/badges/__init__.py +13 -0
- django_cfg/modules/django_admin/utils/{badges.py → badges/status_badges.py} +3 -3
- django_cfg/modules/django_admin/utils/displays/__init__.py +13 -0
- django_cfg/modules/django_admin/utils/{displays.py → displays/data_displays.py} +2 -2
- django_cfg/modules/django_admin/utils/html/__init__.py +26 -0
- django_cfg/modules/django_admin/utils/html/badges.py +47 -0
- django_cfg/modules/django_admin/utils/html/base.py +167 -0
- django_cfg/modules/django_admin/utils/html/code.py +87 -0
- django_cfg/modules/django_admin/utils/html/composition.py +205 -0
- django_cfg/modules/django_admin/utils/html/formatting.py +231 -0
- django_cfg/modules/django_admin/utils/html/keyvalue.py +219 -0
- django_cfg/modules/django_admin/utils/html/markdown_integration.py +108 -0
- django_cfg/modules/django_admin/utils/html/progress.py +127 -0
- django_cfg/modules/django_admin/utils/html_builder.py +55 -408
- django_cfg/modules/django_admin/utils/markdown/__init__.py +21 -0
- django_cfg/modules/django_unfold/navigation.py +21 -18
- django_cfg/pyproject.toml +4 -6
- django_cfg/registry/core.py +4 -7
- django_cfg/registry/modules.py +6 -0
- django_cfg/static/frontend/admin.zip +0 -0
- django_cfg/templates/admin/constance/includes/results_list.html +73 -0
- django_cfg/templates/admin/index.html +187 -62
- django_cfg/templatetags/django_cfg.py +61 -1
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/METADATA +12 -4
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/RECORD +140 -96
- django_cfg/apps/dashboard/permissions.py +0 -48
- django_cfg/apps/dashboard/serializers/django_q2.py +0 -50
- django_cfg/apps/dashboard/services/django_q2_service.py +0 -159
- django_cfg/apps/dashboard/views/django_q2_views.py +0 -79
- django_cfg/apps/tasks/__init__.py +0 -64
- django_cfg/apps/tasks/admin/__init__.py +0 -4
- django_cfg/apps/tasks/admin/task_log.py +0 -265
- django_cfg/apps/tasks/apps.py +0 -15
- django_cfg/apps/tasks/filters/__init__.py +0 -10
- django_cfg/apps/tasks/filters/task_log.py +0 -121
- django_cfg/apps/tasks/migrations/0001_initial.py +0 -196
- django_cfg/apps/tasks/migrations/0002_delete_tasklog.py +0 -16
- django_cfg/apps/tasks/models/__init__.py +0 -4
- django_cfg/apps/tasks/models/task_log.py +0 -246
- django_cfg/apps/tasks/serializers/__init__.py +0 -28
- django_cfg/apps/tasks/serializers/task_log.py +0 -249
- django_cfg/apps/tasks/services/__init__.py +0 -10
- django_cfg/apps/tasks/services/client/__init__.py +0 -7
- django_cfg/apps/tasks/services/client/client.py +0 -234
- django_cfg/apps/tasks/services/config_helper.py +0 -63
- django_cfg/apps/tasks/services/sync.py +0 -204
- django_cfg/apps/tasks/urls.py +0 -16
- django_cfg/apps/tasks/views/__init__.py +0 -10
- django_cfg/apps/tasks/views/task_log.py +0 -41
- django_cfg/apps/tasks/views/task_log_base.py +0 -41
- django_cfg/apps/tasks/views/task_log_overview.py +0 -100
- django_cfg/apps/tasks/views/task_log_related.py +0 -41
- django_cfg/apps/tasks/views/task_log_stats.py +0 -91
- django_cfg/apps/tasks/views/task_log_timeline.py +0 -81
- django_cfg/core/generation/integration_generators/django_q2.py +0 -133
- django_cfg/core/generation/integration_generators/tasks.py +0 -88
- django_cfg/models/django/django_q2.py +0 -514
- django_cfg/models/tasks/__init__.py +0 -49
- django_cfg/models/tasks/backends.py +0 -122
- django_cfg/models/tasks/config.py +0 -209
- django_cfg/models/tasks/utils.py +0 -162
- django_cfg/modules/django_admin/utils/CODE_BLOCK_DOCS.md +0 -396
- django_cfg/modules/django_q2/README.md +0 -140
- django_cfg/modules/django_q2/__init__.py +0 -8
- django_cfg/modules/django_q2/apps.py +0 -107
- django_cfg/modules/django_q2/management/commands/__init__.py +0 -0
- django_cfg/modules/django_q2/management/commands/sync_django_q_schedules.py +0 -74
- /django_cfg/apps/{tasks/migrations → grpc/management/commands}/__init__.py +0 -0
- /django_cfg/{modules/django_q2/management → apps/grpc/migrations}/__init__.py +0 -0
- /django_cfg/modules/django_admin/utils/{mermaid_plugin.py → markdown/mermaid_plugin.py} +0 -0
- /django_cfg/modules/django_admin/utils/{markdown_renderer.py → markdown/renderer.py} +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.120.dist-info → django_cfg-1.5.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django-RQ Testing & Simulation ViewSet.
|
|
3
|
+
|
|
4
|
+
Provides REST API endpoints for testing RQ functionality from the frontend.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
|
|
10
|
+
from django_cfg.mixins import AdminAPIMixin
|
|
11
|
+
from django_cfg.modules.django_logging import get_logger
|
|
12
|
+
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
|
13
|
+
from rest_framework import status, viewsets
|
|
14
|
+
from rest_framework.decorators import action
|
|
15
|
+
from rest_framework.response import Response
|
|
16
|
+
|
|
17
|
+
from ..serializers import (
|
|
18
|
+
TestScenarioSerializer,
|
|
19
|
+
RunDemoRequestSerializer,
|
|
20
|
+
StressTestRequestSerializer,
|
|
21
|
+
TestingActionResponseSerializer,
|
|
22
|
+
CleanupRequestSerializer,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
logger = get_logger("rq.testing")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestingViewSet(AdminAPIMixin, viewsets.ViewSet):
|
|
29
|
+
"""
|
|
30
|
+
ViewSet for RQ testing and simulation.
|
|
31
|
+
|
|
32
|
+
Provides endpoints for:
|
|
33
|
+
- List available test scenarios
|
|
34
|
+
- Run demo tasks
|
|
35
|
+
- Stress testing (generate N jobs)
|
|
36
|
+
- Schedule demo tasks
|
|
37
|
+
- Simulate queue load
|
|
38
|
+
- Cleanup test jobs
|
|
39
|
+
- Get test results
|
|
40
|
+
|
|
41
|
+
Requires admin authentication (JWT, Session, or Basic Auth).
|
|
42
|
+
|
|
43
|
+
Security:
|
|
44
|
+
- Admin-only access via AdminAPIMixin
|
|
45
|
+
- Rate limiting on stress test endpoints
|
|
46
|
+
- Resource limits on memory/CPU intensive tasks
|
|
47
|
+
- Can be disabled in production via config
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
# Scenario definitions
|
|
51
|
+
SCENARIOS = {
|
|
52
|
+
'success': {
|
|
53
|
+
'id': 'success',
|
|
54
|
+
'name': 'Success Task',
|
|
55
|
+
'description': 'Simple task that always succeeds after specified duration',
|
|
56
|
+
'task_func': 'django_cfg.apps.rq.tasks.demo_tasks.demo_success_task',
|
|
57
|
+
'default_args': [],
|
|
58
|
+
'default_kwargs': {'duration': 2, 'message': 'Demo task completed'},
|
|
59
|
+
'estimated_duration': 2,
|
|
60
|
+
},
|
|
61
|
+
'failure': {
|
|
62
|
+
'id': 'failure',
|
|
63
|
+
'name': 'Failure Task',
|
|
64
|
+
'description': 'Task that always fails with ValueError exception',
|
|
65
|
+
'task_func': 'django_cfg.apps.rq.tasks.demo_tasks.demo_failure_task',
|
|
66
|
+
'default_args': [],
|
|
67
|
+
'default_kwargs': {'error_message': 'Simulated failure'},
|
|
68
|
+
'estimated_duration': 1,
|
|
69
|
+
},
|
|
70
|
+
'slow': {
|
|
71
|
+
'id': 'slow',
|
|
72
|
+
'name': 'Slow Task',
|
|
73
|
+
'description': 'Long-running task for testing timeouts and monitoring',
|
|
74
|
+
'task_func': 'django_cfg.apps.rq.tasks.demo_tasks.demo_slow_task',
|
|
75
|
+
'default_args': [],
|
|
76
|
+
'default_kwargs': {'duration': 30, 'step_interval': 5},
|
|
77
|
+
'estimated_duration': 30,
|
|
78
|
+
},
|
|
79
|
+
'progress': {
|
|
80
|
+
'id': 'progress',
|
|
81
|
+
'name': 'Progress Task',
|
|
82
|
+
'description': 'Task that updates progress in job.meta for progress bar testing',
|
|
83
|
+
'task_func': 'django_cfg.apps.rq.tasks.demo_tasks.demo_progress_task',
|
|
84
|
+
'default_args': [],
|
|
85
|
+
'default_kwargs': {'total_items': 100},
|
|
86
|
+
'estimated_duration': 10,
|
|
87
|
+
},
|
|
88
|
+
'retry': {
|
|
89
|
+
'id': 'retry',
|
|
90
|
+
'name': 'Retry Task',
|
|
91
|
+
'description': 'Task that fails N times before succeeding (retry logic testing)',
|
|
92
|
+
'task_func': 'django_cfg.apps.rq.tasks.demo_tasks.demo_retry_task',
|
|
93
|
+
'default_args': [],
|
|
94
|
+
'default_kwargs': {'max_attempts': 3, 'fail_until_attempt': 2},
|
|
95
|
+
'estimated_duration': 3,
|
|
96
|
+
},
|
|
97
|
+
'random': {
|
|
98
|
+
'id': 'random',
|
|
99
|
+
'name': 'Random Task',
|
|
100
|
+
'description': 'Task with configurable success/failure probability',
|
|
101
|
+
'task_func': 'django_cfg.apps.rq.tasks.demo_tasks.demo_random_task',
|
|
102
|
+
'default_args': [],
|
|
103
|
+
'default_kwargs': {'success_rate': 0.7},
|
|
104
|
+
'estimated_duration': 2,
|
|
105
|
+
},
|
|
106
|
+
'memory': {
|
|
107
|
+
'id': 'memory',
|
|
108
|
+
'name': 'Memory Intensive Task',
|
|
109
|
+
'description': 'Task that allocates specified MB of memory',
|
|
110
|
+
'task_func': 'django_cfg.apps.rq.tasks.demo_tasks.demo_memory_intensive_task',
|
|
111
|
+
'default_args': [],
|
|
112
|
+
'default_kwargs': {'mb_to_allocate': 100},
|
|
113
|
+
'estimated_duration': 5,
|
|
114
|
+
},
|
|
115
|
+
'cpu': {
|
|
116
|
+
'id': 'cpu',
|
|
117
|
+
'name': 'CPU Intensive Task',
|
|
118
|
+
'description': 'Task with heavy CPU computation',
|
|
119
|
+
'task_func': 'django_cfg.apps.rq.tasks.demo_tasks.demo_cpu_intensive_task',
|
|
120
|
+
'default_args': [],
|
|
121
|
+
'default_kwargs': {'iterations': 1000000},
|
|
122
|
+
'estimated_duration': 10,
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@extend_schema(
|
|
127
|
+
tags=["RQ Testing"],
|
|
128
|
+
summary="List test scenarios",
|
|
129
|
+
description="Returns list of all available test scenarios with metadata.",
|
|
130
|
+
responses={
|
|
131
|
+
200: TestScenarioSerializer(many=True),
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
def list(self, request):
|
|
135
|
+
"""List all available test scenarios."""
|
|
136
|
+
try:
|
|
137
|
+
scenarios = list(self.SCENARIOS.values())
|
|
138
|
+
serializer = TestScenarioSerializer(data=scenarios, many=True)
|
|
139
|
+
serializer.is_valid(raise_exception=True)
|
|
140
|
+
return Response(serializer.data)
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.error(f"Failed to list scenarios: {e}", exc_info=True)
|
|
144
|
+
return Response(
|
|
145
|
+
{"error": "Internal server error"},
|
|
146
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
@extend_schema(
|
|
150
|
+
tags=["RQ Testing"],
|
|
151
|
+
summary="Run demo task",
|
|
152
|
+
description="Enqueue a single demo task for testing.",
|
|
153
|
+
request=RunDemoRequestSerializer,
|
|
154
|
+
responses={
|
|
155
|
+
200: TestingActionResponseSerializer,
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
@action(detail=False, methods=["post"], url_path="run-demo")
|
|
159
|
+
def run_demo(self, request):
|
|
160
|
+
"""Run a single demo task."""
|
|
161
|
+
try:
|
|
162
|
+
import django_rq
|
|
163
|
+
|
|
164
|
+
serializer = RunDemoRequestSerializer(data=request.data)
|
|
165
|
+
serializer.is_valid(raise_exception=True)
|
|
166
|
+
data = serializer.validated_data
|
|
167
|
+
|
|
168
|
+
scenario_id = data['scenario']
|
|
169
|
+
queue_name = data.get('queue', 'default')
|
|
170
|
+
|
|
171
|
+
# Get scenario config
|
|
172
|
+
scenario = self.SCENARIOS.get(scenario_id)
|
|
173
|
+
if not scenario:
|
|
174
|
+
return Response(
|
|
175
|
+
{"error": f"Unknown scenario: {scenario_id}"},
|
|
176
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Import task function
|
|
180
|
+
task_func_path = scenario['task_func']
|
|
181
|
+
module_path, func_name = task_func_path.rsplit('.', 1)
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
import importlib
|
|
185
|
+
module = importlib.import_module(module_path)
|
|
186
|
+
task_func = getattr(module, func_name)
|
|
187
|
+
except (ImportError, AttributeError) as e:
|
|
188
|
+
logger.error(f"Failed to import task {task_func_path}: {e}")
|
|
189
|
+
return Response(
|
|
190
|
+
{"error": f"Failed to import task function: {str(e)}"},
|
|
191
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Merge kwargs (user overrides defaults)
|
|
195
|
+
kwargs = {**scenario['default_kwargs'], **data.get('kwargs', {})}
|
|
196
|
+
args = data.get('args', [])
|
|
197
|
+
|
|
198
|
+
# Get queue
|
|
199
|
+
queue = django_rq.get_queue(queue_name)
|
|
200
|
+
|
|
201
|
+
# Enqueue job
|
|
202
|
+
job = queue.enqueue(
|
|
203
|
+
task_func,
|
|
204
|
+
args=args,
|
|
205
|
+
kwargs=kwargs,
|
|
206
|
+
timeout=data.get('timeout'),
|
|
207
|
+
meta={
|
|
208
|
+
'source': 'testing',
|
|
209
|
+
'scenario': scenario_id,
|
|
210
|
+
'created_via': 'testing_api',
|
|
211
|
+
'timestamp': datetime.now(timezone.utc).isoformat(),
|
|
212
|
+
},
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
response_data = {
|
|
216
|
+
"success": True,
|
|
217
|
+
"message": f"Demo task '{scenario['name']}' enqueued successfully",
|
|
218
|
+
"job_ids": [job.id],
|
|
219
|
+
"metadata": {
|
|
220
|
+
"scenario": scenario_id,
|
|
221
|
+
"queue": queue_name,
|
|
222
|
+
"func": task_func_path,
|
|
223
|
+
"estimated_duration": scenario.get('estimated_duration'),
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
logger.info(f"Enqueued demo task: {scenario_id} -> {job.id}")
|
|
228
|
+
|
|
229
|
+
response_serializer = TestingActionResponseSerializer(data=response_data)
|
|
230
|
+
response_serializer.is_valid(raise_exception=True)
|
|
231
|
+
return Response(response_serializer.data)
|
|
232
|
+
|
|
233
|
+
except Exception as e:
|
|
234
|
+
logger.error(f"Failed to run demo task: {e}", exc_info=True)
|
|
235
|
+
return Response(
|
|
236
|
+
{"error": str(e)},
|
|
237
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
@extend_schema(
|
|
241
|
+
tags=["RQ Testing"],
|
|
242
|
+
summary="Stress test",
|
|
243
|
+
description="Generate N jobs for load testing and performance benchmarking.",
|
|
244
|
+
request=StressTestRequestSerializer,
|
|
245
|
+
responses={
|
|
246
|
+
200: TestingActionResponseSerializer,
|
|
247
|
+
},
|
|
248
|
+
)
|
|
249
|
+
@action(detail=False, methods=["post"], url_path="stress-test")
|
|
250
|
+
def stress_test(self, request):
|
|
251
|
+
"""Generate N jobs for stress testing."""
|
|
252
|
+
try:
|
|
253
|
+
import django_rq
|
|
254
|
+
|
|
255
|
+
serializer = StressTestRequestSerializer(data=request.data)
|
|
256
|
+
serializer.is_valid(raise_exception=True)
|
|
257
|
+
data = serializer.validated_data
|
|
258
|
+
|
|
259
|
+
num_jobs = data['num_jobs']
|
|
260
|
+
queue_name = data.get('queue', 'default')
|
|
261
|
+
scenario_id = data.get('scenario', 'success')
|
|
262
|
+
duration = data.get('duration', 2)
|
|
263
|
+
|
|
264
|
+
# Get scenario
|
|
265
|
+
scenario = self.SCENARIOS.get(scenario_id)
|
|
266
|
+
if not scenario:
|
|
267
|
+
return Response(
|
|
268
|
+
{"error": f"Unknown scenario: {scenario_id}"},
|
|
269
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Import task function
|
|
273
|
+
task_func_path = scenario['task_func']
|
|
274
|
+
module_path, func_name = task_func_path.rsplit('.', 1)
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
import importlib
|
|
278
|
+
module = importlib.import_module(module_path)
|
|
279
|
+
task_func = getattr(module, func_name)
|
|
280
|
+
except (ImportError, AttributeError) as e:
|
|
281
|
+
return Response(
|
|
282
|
+
{"error": f"Failed to import task: {str(e)}"},
|
|
283
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Get queue
|
|
287
|
+
queue = django_rq.get_queue(queue_name)
|
|
288
|
+
|
|
289
|
+
# Enqueue jobs
|
|
290
|
+
job_ids = []
|
|
291
|
+
for i in range(num_jobs):
|
|
292
|
+
# Override duration for stress test
|
|
293
|
+
kwargs = {**scenario['default_kwargs'], 'duration': duration}
|
|
294
|
+
|
|
295
|
+
job = queue.enqueue(
|
|
296
|
+
task_func,
|
|
297
|
+
kwargs=kwargs,
|
|
298
|
+
meta={
|
|
299
|
+
'source': 'testing',
|
|
300
|
+
'scenario': scenario_id,
|
|
301
|
+
'created_via': 'stress_test',
|
|
302
|
+
'test_index': i + 1,
|
|
303
|
+
'timestamp': datetime.now(timezone.utc).isoformat(),
|
|
304
|
+
},
|
|
305
|
+
)
|
|
306
|
+
job_ids.append(job.id)
|
|
307
|
+
|
|
308
|
+
estimated_total_time = num_jobs * duration
|
|
309
|
+
|
|
310
|
+
response_data = {
|
|
311
|
+
"success": True,
|
|
312
|
+
"message": f"Created {num_jobs} stress test jobs",
|
|
313
|
+
"job_ids": job_ids,
|
|
314
|
+
"count": num_jobs,
|
|
315
|
+
"metadata": {
|
|
316
|
+
"scenario": scenario_id,
|
|
317
|
+
"queue": queue_name,
|
|
318
|
+
"duration_per_job": duration,
|
|
319
|
+
"estimated_total_time": estimated_total_time,
|
|
320
|
+
},
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
logger.info(f"Created {num_jobs} stress test jobs for scenario '{scenario_id}'")
|
|
324
|
+
|
|
325
|
+
response_serializer = TestingActionResponseSerializer(data=response_data)
|
|
326
|
+
response_serializer.is_valid(raise_exception=True)
|
|
327
|
+
return Response(response_serializer.data)
|
|
328
|
+
|
|
329
|
+
except Exception as e:
|
|
330
|
+
logger.error(f"Stress test failed: {e}", exc_info=True)
|
|
331
|
+
return Response(
|
|
332
|
+
{"error": str(e)},
|
|
333
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
@extend_schema(
|
|
337
|
+
tags=["RQ Testing"],
|
|
338
|
+
summary="Schedule demo tasks",
|
|
339
|
+
description="Register demo scheduled tasks using rq-scheduler.",
|
|
340
|
+
request={
|
|
341
|
+
'application/json': {
|
|
342
|
+
'type': 'object',
|
|
343
|
+
'properties': {
|
|
344
|
+
'scenario': {'type': 'string'},
|
|
345
|
+
'schedule_type': {'type': 'string', 'enum': ['interval', 'cron', 'one-time']},
|
|
346
|
+
'interval': {'type': 'integer'},
|
|
347
|
+
'cron': {'type': 'string'},
|
|
348
|
+
'scheduled_time': {'type': 'string', 'format': 'date-time'},
|
|
349
|
+
'queue': {'type': 'string'},
|
|
350
|
+
'repeat': {'type': 'integer'},
|
|
351
|
+
},
|
|
352
|
+
'required': ['scenario', 'schedule_type'],
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
responses={
|
|
356
|
+
200: TestingActionResponseSerializer,
|
|
357
|
+
},
|
|
358
|
+
)
|
|
359
|
+
@action(detail=False, methods=["post"], url_path="schedule-demo")
|
|
360
|
+
def schedule_demo(self, request):
|
|
361
|
+
"""Schedule a demo task."""
|
|
362
|
+
try:
|
|
363
|
+
import django_rq
|
|
364
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
365
|
+
|
|
366
|
+
data = request.data
|
|
367
|
+
scenario_id = data.get('scenario')
|
|
368
|
+
schedule_type = data.get('schedule_type')
|
|
369
|
+
queue_name = data.get('queue', 'default')
|
|
370
|
+
|
|
371
|
+
# Validate scenario
|
|
372
|
+
scenario = self.SCENARIOS.get(scenario_id)
|
|
373
|
+
if not scenario:
|
|
374
|
+
return Response(
|
|
375
|
+
{"error": f"Unknown scenario: {scenario_id}"},
|
|
376
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# Import task function
|
|
380
|
+
task_func_path = scenario['task_func']
|
|
381
|
+
module_path, func_name = task_func_path.rsplit('.', 1)
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
import importlib
|
|
385
|
+
module = importlib.import_module(module_path)
|
|
386
|
+
task_func = getattr(module, func_name)
|
|
387
|
+
except (ImportError, AttributeError) as e:
|
|
388
|
+
return Response(
|
|
389
|
+
{"error": f"Failed to import task: {str(e)}"},
|
|
390
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# Get scheduler
|
|
394
|
+
try:
|
|
395
|
+
scheduler = django_rq.get_scheduler(queue_name)
|
|
396
|
+
except ImproperlyConfigured:
|
|
397
|
+
return Response(
|
|
398
|
+
{"error": "rq-scheduler not installed. Install with: pip install rq-scheduler"},
|
|
399
|
+
status=status.HTTP_501_NOT_IMPLEMENTED,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# Schedule job based on type
|
|
403
|
+
job_kwargs = {
|
|
404
|
+
'func': task_func,
|
|
405
|
+
'kwargs': scenario['default_kwargs'],
|
|
406
|
+
'queue_name': queue_name,
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if data.get('repeat') is not None:
|
|
410
|
+
job_kwargs['repeat'] = data['repeat']
|
|
411
|
+
|
|
412
|
+
if schedule_type == 'interval':
|
|
413
|
+
interval = data.get('interval')
|
|
414
|
+
if not interval:
|
|
415
|
+
return Response(
|
|
416
|
+
{"error": "interval is required for interval schedule"},
|
|
417
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
job = scheduler.schedule(
|
|
421
|
+
scheduled_time=datetime.now(timezone.utc),
|
|
422
|
+
interval=interval,
|
|
423
|
+
**job_kwargs
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
elif schedule_type == 'cron':
|
|
427
|
+
cron_string = data.get('cron')
|
|
428
|
+
if not cron_string:
|
|
429
|
+
return Response(
|
|
430
|
+
{"error": "cron is required for cron schedule"},
|
|
431
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
job = scheduler.cron(cron_string, **job_kwargs)
|
|
435
|
+
|
|
436
|
+
elif schedule_type == 'one-time':
|
|
437
|
+
scheduled_time = data.get('scheduled_time')
|
|
438
|
+
if not scheduled_time:
|
|
439
|
+
return Response(
|
|
440
|
+
{"error": "scheduled_time is required for one-time schedule"},
|
|
441
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
scheduled_dt = datetime.fromisoformat(scheduled_time.replace('Z', '+00:00'))
|
|
445
|
+
job = scheduler.enqueue_at(scheduled_dt, **job_kwargs)
|
|
446
|
+
|
|
447
|
+
else:
|
|
448
|
+
return Response(
|
|
449
|
+
{"error": f"Invalid schedule_type: {schedule_type}"},
|
|
450
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
# Update job meta
|
|
454
|
+
if job:
|
|
455
|
+
job.meta = {
|
|
456
|
+
'source': 'testing',
|
|
457
|
+
'scenario': scenario_id,
|
|
458
|
+
'created_via': 'schedule_demo',
|
|
459
|
+
'schedule_type': schedule_type,
|
|
460
|
+
}
|
|
461
|
+
job.save_meta()
|
|
462
|
+
|
|
463
|
+
response_data = {
|
|
464
|
+
"success": True,
|
|
465
|
+
"message": f"Demo schedule registered ({schedule_type})",
|
|
466
|
+
"job_ids": [job.id] if job else [],
|
|
467
|
+
"metadata": {
|
|
468
|
+
"schedule_type": schedule_type,
|
|
469
|
+
"scenario": scenario_id,
|
|
470
|
+
"queue": queue_name,
|
|
471
|
+
},
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
logger.info(f"Scheduled demo task: {scenario_id} ({schedule_type})")
|
|
475
|
+
|
|
476
|
+
response_serializer = TestingActionResponseSerializer(data=response_data)
|
|
477
|
+
response_serializer.is_valid(raise_exception=True)
|
|
478
|
+
return Response(response_serializer.data)
|
|
479
|
+
|
|
480
|
+
except Exception as e:
|
|
481
|
+
logger.error(f"Failed to schedule demo: {e}", exc_info=True)
|
|
482
|
+
return Response(
|
|
483
|
+
{"error": str(e)},
|
|
484
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
@extend_schema(
|
|
488
|
+
tags=["RQ Testing"],
|
|
489
|
+
summary="Cleanup test jobs",
|
|
490
|
+
description="Clean demo jobs from registries.",
|
|
491
|
+
request=CleanupRequestSerializer,
|
|
492
|
+
parameters=[
|
|
493
|
+
OpenApiParameter(
|
|
494
|
+
name="queue",
|
|
495
|
+
type=str,
|
|
496
|
+
location=OpenApiParameter.QUERY,
|
|
497
|
+
required=False,
|
|
498
|
+
description="Queue name (empty = all queues)",
|
|
499
|
+
),
|
|
500
|
+
OpenApiParameter(
|
|
501
|
+
name="registries",
|
|
502
|
+
type=str,
|
|
503
|
+
location=OpenApiParameter.QUERY,
|
|
504
|
+
required=False,
|
|
505
|
+
description="Comma-separated list of registries (failed,finished,deferred,scheduled)",
|
|
506
|
+
),
|
|
507
|
+
OpenApiParameter(
|
|
508
|
+
name="delete_demo_jobs_only",
|
|
509
|
+
type=bool,
|
|
510
|
+
location=OpenApiParameter.QUERY,
|
|
511
|
+
required=False,
|
|
512
|
+
description="Only delete demo jobs (default: true)",
|
|
513
|
+
),
|
|
514
|
+
],
|
|
515
|
+
responses={
|
|
516
|
+
200: TestingActionResponseSerializer,
|
|
517
|
+
},
|
|
518
|
+
)
|
|
519
|
+
@action(detail=False, methods=["delete"])
|
|
520
|
+
def cleanup(self, request):
|
|
521
|
+
"""Cleanup test jobs from registries."""
|
|
522
|
+
try:
|
|
523
|
+
import django_rq
|
|
524
|
+
from django.conf import settings
|
|
525
|
+
from rq.job import Job
|
|
526
|
+
from rq.registry import (
|
|
527
|
+
FinishedJobRegistry,
|
|
528
|
+
FailedJobRegistry,
|
|
529
|
+
DeferredJobRegistry,
|
|
530
|
+
ScheduledJobRegistry,
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
queue_filter = request.query_params.get('queue')
|
|
534
|
+
registries_param = request.query_params.get('registries', 'failed,finished')
|
|
535
|
+
delete_demo_only = request.query_params.get('delete_demo_jobs_only', 'true').lower() == 'true'
|
|
536
|
+
|
|
537
|
+
# Parse registries
|
|
538
|
+
registry_names = [r.strip() for r in registries_param.split(',') if r.strip()]
|
|
539
|
+
|
|
540
|
+
# Get queues
|
|
541
|
+
if queue_filter:
|
|
542
|
+
queue_names = [queue_filter]
|
|
543
|
+
else:
|
|
544
|
+
queue_names = list(settings.RQ_QUEUES.keys()) if hasattr(settings, 'RQ_QUEUES') else []
|
|
545
|
+
|
|
546
|
+
total_deleted = 0
|
|
547
|
+
breakdown = {}
|
|
548
|
+
|
|
549
|
+
for queue_name in queue_names:
|
|
550
|
+
try:
|
|
551
|
+
queue = django_rq.get_queue(queue_name)
|
|
552
|
+
|
|
553
|
+
# Get registries to clean
|
|
554
|
+
registries_to_clean = {}
|
|
555
|
+
if 'failed' in registry_names:
|
|
556
|
+
registries_to_clean['failed'] = FailedJobRegistry(queue_name, connection=queue.connection)
|
|
557
|
+
if 'finished' in registry_names:
|
|
558
|
+
registries_to_clean['finished'] = FinishedJobRegistry(queue_name, connection=queue.connection)
|
|
559
|
+
if 'deferred' in registry_names:
|
|
560
|
+
registries_to_clean['deferred'] = DeferredJobRegistry(queue_name, connection=queue.connection)
|
|
561
|
+
if 'scheduled' in registry_names:
|
|
562
|
+
registries_to_clean['scheduled'] = ScheduledJobRegistry(queue_name, connection=queue.connection)
|
|
563
|
+
|
|
564
|
+
# Clean from registries
|
|
565
|
+
for reg_name, registry in registries_to_clean.items():
|
|
566
|
+
job_ids = registry.get_job_ids()
|
|
567
|
+
deleted_count = 0
|
|
568
|
+
|
|
569
|
+
for job_id in job_ids:
|
|
570
|
+
try:
|
|
571
|
+
job = Job.fetch(job_id, connection=queue.connection)
|
|
572
|
+
|
|
573
|
+
# Check if it's a demo job
|
|
574
|
+
is_demo_job = False
|
|
575
|
+
if job.meta and job.meta.get('source') == 'testing':
|
|
576
|
+
is_demo_job = True
|
|
577
|
+
elif job.func_name and 'demo_' in job.func_name:
|
|
578
|
+
is_demo_job = True
|
|
579
|
+
|
|
580
|
+
# Delete if matches criteria
|
|
581
|
+
if not delete_demo_only or is_demo_job:
|
|
582
|
+
registry.remove(job, delete_job=True)
|
|
583
|
+
deleted_count += 1
|
|
584
|
+
|
|
585
|
+
except Exception as e:
|
|
586
|
+
logger.debug(f"Failed to delete job {job_id}: {e}")
|
|
587
|
+
|
|
588
|
+
breakdown[reg_name] = breakdown.get(reg_name, 0) + deleted_count
|
|
589
|
+
total_deleted += deleted_count
|
|
590
|
+
|
|
591
|
+
# Also clean from queued jobs (not in registry, but in queue itself)
|
|
592
|
+
if 'queued' in registry_names:
|
|
593
|
+
queued_job_ids = queue.job_ids
|
|
594
|
+
deleted_count = 0
|
|
595
|
+
|
|
596
|
+
for job_id in queued_job_ids:
|
|
597
|
+
try:
|
|
598
|
+
job = Job.fetch(job_id, connection=queue.connection)
|
|
599
|
+
|
|
600
|
+
# Check if it's a demo job
|
|
601
|
+
is_demo_job = False
|
|
602
|
+
if job.meta and job.meta.get('source') == 'testing':
|
|
603
|
+
is_demo_job = True
|
|
604
|
+
elif job.func_name and 'demo_' in job.func_name:
|
|
605
|
+
is_demo_job = True
|
|
606
|
+
|
|
607
|
+
# Delete if matches criteria
|
|
608
|
+
if not delete_demo_only or is_demo_job:
|
|
609
|
+
job.cancel()
|
|
610
|
+
job.delete()
|
|
611
|
+
deleted_count += 1
|
|
612
|
+
|
|
613
|
+
except Exception as e:
|
|
614
|
+
logger.debug(f"Failed to delete queued job {job_id}: {e}")
|
|
615
|
+
|
|
616
|
+
breakdown['queued'] = breakdown.get('queued', 0) + deleted_count
|
|
617
|
+
total_deleted += deleted_count
|
|
618
|
+
|
|
619
|
+
except Exception as e:
|
|
620
|
+
logger.debug(f"Failed to clean queue {queue_name}: {e}")
|
|
621
|
+
|
|
622
|
+
response_data = {
|
|
623
|
+
"success": True,
|
|
624
|
+
"message": f"Cleaned {total_deleted} {'demo ' if delete_demo_only else ''}jobs",
|
|
625
|
+
"count": total_deleted,
|
|
626
|
+
"metadata": {
|
|
627
|
+
"queues_cleaned": queue_names,
|
|
628
|
+
"registries_cleaned": registry_names,
|
|
629
|
+
"demo_jobs_only": delete_demo_only,
|
|
630
|
+
"breakdown": breakdown,
|
|
631
|
+
},
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
logger.info(f"Cleaned {total_deleted} test jobs from {len(queue_names)} queues")
|
|
635
|
+
|
|
636
|
+
response_serializer = TestingActionResponseSerializer(data=response_data)
|
|
637
|
+
response_serializer.is_valid(raise_exception=True)
|
|
638
|
+
return Response(response_serializer.data)
|
|
639
|
+
|
|
640
|
+
except Exception as e:
|
|
641
|
+
logger.error(f"Cleanup failed: {e}", exc_info=True)
|
|
642
|
+
return Response(
|
|
643
|
+
{"error": str(e)},
|
|
644
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
@extend_schema(
|
|
648
|
+
tags=["RQ Testing"],
|
|
649
|
+
summary="Get test results",
|
|
650
|
+
description="Get aggregated results of test jobs execution.",
|
|
651
|
+
parameters=[
|
|
652
|
+
OpenApiParameter(
|
|
653
|
+
name="queue",
|
|
654
|
+
type=str,
|
|
655
|
+
location=OpenApiParameter.QUERY,
|
|
656
|
+
required=False,
|
|
657
|
+
description="Filter by queue",
|
|
658
|
+
),
|
|
659
|
+
OpenApiParameter(
|
|
660
|
+
name="scenario",
|
|
661
|
+
type=str,
|
|
662
|
+
location=OpenApiParameter.QUERY,
|
|
663
|
+
required=False,
|
|
664
|
+
description="Filter by scenario",
|
|
665
|
+
),
|
|
666
|
+
],
|
|
667
|
+
responses={
|
|
668
|
+
200: {
|
|
669
|
+
'type': 'object',
|
|
670
|
+
'properties': {
|
|
671
|
+
'total_jobs': {'type': 'integer'},
|
|
672
|
+
'by_status': {'type': 'object'},
|
|
673
|
+
'by_scenario': {'type': 'object'},
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
)
|
|
678
|
+
@action(detail=False, methods=["get"])
|
|
679
|
+
def results(self, request):
|
|
680
|
+
"""Get test execution results."""
|
|
681
|
+
try:
|
|
682
|
+
import django_rq
|
|
683
|
+
from django.conf import settings
|
|
684
|
+
from rq.job import Job
|
|
685
|
+
from rq.registry import (
|
|
686
|
+
FinishedJobRegistry,
|
|
687
|
+
FailedJobRegistry,
|
|
688
|
+
StartedJobRegistry,
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
queue_filter = request.query_params.get('queue')
|
|
692
|
+
scenario_filter = request.query_params.get('scenario')
|
|
693
|
+
|
|
694
|
+
queue_names = list(settings.RQ_QUEUES.keys()) if hasattr(settings, 'RQ_QUEUES') else []
|
|
695
|
+
if queue_filter:
|
|
696
|
+
queue_names = [q for q in queue_names if q == queue_filter]
|
|
697
|
+
|
|
698
|
+
total_jobs = 0
|
|
699
|
+
by_status = {'finished': 0, 'failed': 0, 'queued': 0, 'started': 0}
|
|
700
|
+
by_scenario = {}
|
|
701
|
+
|
|
702
|
+
for queue_name in queue_names:
|
|
703
|
+
try:
|
|
704
|
+
queue = django_rq.get_queue(queue_name)
|
|
705
|
+
|
|
706
|
+
# Check all registries
|
|
707
|
+
registries = {
|
|
708
|
+
'queued': {'jobs': queue.job_ids},
|
|
709
|
+
'started': {'registry': StartedJobRegistry(queue_name, connection=queue.connection)},
|
|
710
|
+
'finished': {'registry': FinishedJobRegistry(queue_name, connection=queue.connection)},
|
|
711
|
+
'failed': {'registry': FailedJobRegistry(queue_name, connection=queue.connection)},
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
for reg_name, reg_data in registries.items():
|
|
715
|
+
if 'registry' in reg_data:
|
|
716
|
+
job_ids = reg_data['registry'].get_job_ids()
|
|
717
|
+
else:
|
|
718
|
+
job_ids = reg_data['jobs']
|
|
719
|
+
|
|
720
|
+
for job_id in job_ids:
|
|
721
|
+
try:
|
|
722
|
+
job = Job.fetch(job_id, connection=queue.connection)
|
|
723
|
+
|
|
724
|
+
# Filter demo jobs only
|
|
725
|
+
if not job.meta or job.meta.get('source') != 'testing':
|
|
726
|
+
continue
|
|
727
|
+
|
|
728
|
+
scenario = job.meta.get('scenario', 'unknown')
|
|
729
|
+
|
|
730
|
+
# Apply scenario filter
|
|
731
|
+
if scenario_filter and scenario != scenario_filter:
|
|
732
|
+
continue
|
|
733
|
+
|
|
734
|
+
total_jobs += 1
|
|
735
|
+
by_status[reg_name] += 1
|
|
736
|
+
|
|
737
|
+
if scenario not in by_scenario:
|
|
738
|
+
by_scenario[scenario] = {'total': 0, 'finished': 0, 'failed': 0}
|
|
739
|
+
|
|
740
|
+
by_scenario[scenario]['total'] += 1
|
|
741
|
+
if reg_name in ['finished', 'failed']:
|
|
742
|
+
by_scenario[scenario][reg_name] += 1
|
|
743
|
+
|
|
744
|
+
except Exception:
|
|
745
|
+
continue
|
|
746
|
+
|
|
747
|
+
except Exception as e:
|
|
748
|
+
logger.debug(f"Failed to get results for queue {queue_name}: {e}")
|
|
749
|
+
|
|
750
|
+
return Response({
|
|
751
|
+
"total_jobs": total_jobs,
|
|
752
|
+
"by_status": by_status,
|
|
753
|
+
"by_scenario": by_scenario,
|
|
754
|
+
})
|
|
755
|
+
|
|
756
|
+
except Exception as e:
|
|
757
|
+
logger.error(f"Failed to get results: {e}", exc_info=True)
|
|
758
|
+
return Response(
|
|
759
|
+
{"error": "Internal server error"},
|
|
760
|
+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
761
|
+
)
|