aegis-stack 0.1.0__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 aegis-stack might be problematic. Click here for more details.
- aegis/__init__.py +5 -0
- aegis/__main__.py +374 -0
- aegis/core/CLAUDE.md +365 -0
- aegis/core/__init__.py +6 -0
- aegis/core/components.py +115 -0
- aegis/core/dependency_resolver.py +119 -0
- aegis/core/template_generator.py +163 -0
- aegis/templates/CLAUDE.md +306 -0
- aegis/templates/cookiecutter-aegis-project/cookiecutter.json +27 -0
- aegis/templates/cookiecutter-aegis-project/hooks/post_gen_project.py +172 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.dockerignore +71 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.env.example.j2 +70 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.gitignore +127 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Dockerfile +53 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Makefile +211 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/README.md.j2 +196 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/__init__.py +5 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/__init__.py +6 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/health.py +321 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/load_test.py +638 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/main.py +41 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/health.py +134 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/models.py.j2 +247 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/routing.py.j2 +14 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/tasks.py.j2 +596 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/hooks.py +133 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/main.py +16 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/cors.py +20 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/cleanup.py +14 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/component_health.py.j2 +190 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/core/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/core/theme.py +46 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/main.py +687 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/main.py +138 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/CLAUDE.md +213 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/__init__.py +6 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/constants.py.j2 +30 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/pools.py +78 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/load_test.py +48 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/media.py +41 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/system.py +36 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/registry.py +139 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/__init__.py +119 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/load_tasks.py +526 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/simple_system_tasks.py +32 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/system_tasks.py +279 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/config.py.j2 +119 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/constants.py +60 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/db.py +67 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/log.py +85 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/webserver.py +40 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/{% if cookiecutter.include_scheduler == /"yes/" %}scheduler.py{% endif %}" +21 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/main.py +61 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/py.typed +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test.py +661 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test_models.py +269 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/__init__.py +15 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/models.py +26 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/__init__.py +52 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/alerts.py +94 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/health.py.j2 +1105 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/models.py +169 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/ui.py +52 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docker-compose.yml.j2 +195 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/api.md +191 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md +414 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/development.md +215 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/health.md +240 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/javascripts/mermaid-config.js +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/stylesheets/mermaid.css +95 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/mkdocs.yml.j2 +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/pyproject.toml.j2 +156 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh +87 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh.j2 +104 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/gen_docs.py +16 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_health_endpoints.py.j2 +239 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/components/test_scheduler.py +76 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/conftest.py.j2 +81 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_component_integration.py.j2 +376 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_health_logic.py.j2 +633 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_models.py +665 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_service.py +602 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_system_service.py +96 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_worker_health_registration.py.j2 +224 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/test_core.py +50 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/uv.lock +1673 -0
- aegis_stack-0.1.0.dist-info/METADATA +114 -0
- aegis_stack-0.1.0.dist-info/RECORD +103 -0
- aegis_stack-0.1.0.dist-info/WHEEL +4 -0
- aegis_stack-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Scheduler component for {{ cookiecutter.project_name }}.
|
|
3
|
+
|
|
4
|
+
Simple, explicit job scheduling - just import functions and schedule them.
|
|
5
|
+
Add your own jobs by importing service functions and calling scheduler.add_job().
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
|
|
10
|
+
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
11
|
+
|
|
12
|
+
from app.core.log import logger
|
|
13
|
+
from app.services.system.health import check_system_status, register_health_check
|
|
14
|
+
from app.services.system.models import ComponentStatus, ComponentStatusType
|
|
15
|
+
|
|
16
|
+
# Global scheduler instance for health checking
|
|
17
|
+
_scheduler: AsyncIOScheduler | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def _check_scheduler_health() -> ComponentStatus:
|
|
21
|
+
"""Health check for the scheduler component."""
|
|
22
|
+
global _scheduler
|
|
23
|
+
|
|
24
|
+
if _scheduler is None:
|
|
25
|
+
return ComponentStatus(
|
|
26
|
+
name="scheduler",
|
|
27
|
+
status=ComponentStatusType.UNHEALTHY,
|
|
28
|
+
message="Scheduler not initialized",
|
|
29
|
+
response_time_ms=None,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if not _scheduler.running:
|
|
33
|
+
return ComponentStatus(
|
|
34
|
+
name="scheduler",
|
|
35
|
+
status=ComponentStatusType.UNHEALTHY,
|
|
36
|
+
message="Scheduler is not running",
|
|
37
|
+
response_time_ms=None,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Get scheduler statistics
|
|
41
|
+
jobs = _scheduler.get_jobs()
|
|
42
|
+
job_count = len(jobs)
|
|
43
|
+
|
|
44
|
+
# Check if scheduler is responsive
|
|
45
|
+
try:
|
|
46
|
+
state = _scheduler.state
|
|
47
|
+
healthy = state == 1 # STATE_RUNNING = 1
|
|
48
|
+
|
|
49
|
+
status = (
|
|
50
|
+
ComponentStatusType.HEALTHY
|
|
51
|
+
if healthy
|
|
52
|
+
else ComponentStatusType.UNHEALTHY
|
|
53
|
+
)
|
|
54
|
+
return ComponentStatus(
|
|
55
|
+
name="scheduler",
|
|
56
|
+
status=status,
|
|
57
|
+
message=f"Scheduler running with {job_count} jobs",
|
|
58
|
+
response_time_ms=None,
|
|
59
|
+
metadata={
|
|
60
|
+
"job_count": job_count,
|
|
61
|
+
"state": state,
|
|
62
|
+
"jobs": [{"id": job.id, "name": job.name} for job in jobs[:5]],
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
return ComponentStatus(
|
|
67
|
+
name="scheduler",
|
|
68
|
+
status=ComponentStatusType.UNHEALTHY,
|
|
69
|
+
message=f"Scheduler health check failed: {str(e)}",
|
|
70
|
+
response_time_ms=None,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def create_scheduler() -> AsyncIOScheduler:
|
|
75
|
+
"""Create and configure the scheduler with all jobs."""
|
|
76
|
+
scheduler = AsyncIOScheduler()
|
|
77
|
+
|
|
78
|
+
# ============================================================================
|
|
79
|
+
# JOB SCHEDULE CONFIGURATION
|
|
80
|
+
# Add your scheduled jobs below - import service functions and schedule them!
|
|
81
|
+
# ============================================================================
|
|
82
|
+
|
|
83
|
+
# System health check every 5 minutes
|
|
84
|
+
# Adjust this interval based on your monitoring needs:
|
|
85
|
+
# - Production systems: 1-5 minutes
|
|
86
|
+
# - Development: 10-15 minutes
|
|
87
|
+
# - High-availability: 30 seconds - 1 minute
|
|
88
|
+
scheduler.add_job(
|
|
89
|
+
check_system_status,
|
|
90
|
+
trigger="interval",
|
|
91
|
+
minutes=1,
|
|
92
|
+
id="system_status_check",
|
|
93
|
+
name="System Health Check",
|
|
94
|
+
max_instances=1, # Prevent overlapping health checks
|
|
95
|
+
coalesce=True, # Coalesce missed executions
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Add your own scheduled jobs here by importing service functions
|
|
99
|
+
# and calling scheduler.add_job() with your custom business logic
|
|
100
|
+
|
|
101
|
+
return scheduler
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def run_scheduler() -> None:
|
|
105
|
+
"""Main scheduler runner with lifecycle management."""
|
|
106
|
+
global _scheduler
|
|
107
|
+
|
|
108
|
+
logger.info("🕒 Starting {{ cookiecutter.project_name }} Scheduler")
|
|
109
|
+
|
|
110
|
+
scheduler = create_scheduler()
|
|
111
|
+
_scheduler = scheduler # Store for health checking
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
scheduler.start()
|
|
115
|
+
logger.info("✅ Scheduler started successfully")
|
|
116
|
+
logger.info(f"📋 {len(scheduler.get_jobs())} jobs scheduled:")
|
|
117
|
+
|
|
118
|
+
for job in scheduler.get_jobs():
|
|
119
|
+
logger.info(f" • {job.name} - {job.trigger}")
|
|
120
|
+
|
|
121
|
+
# Register scheduler health check with the system health service
|
|
122
|
+
register_health_check("scheduler", _check_scheduler_health)
|
|
123
|
+
logger.info("🩺 Scheduler health check registered")
|
|
124
|
+
|
|
125
|
+
# Keep the scheduler running
|
|
126
|
+
while True:
|
|
127
|
+
await asyncio.sleep(1)
|
|
128
|
+
|
|
129
|
+
except KeyboardInterrupt:
|
|
130
|
+
logger.info("🛑 Received shutdown signal")
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.error(f"❌ Scheduler error: {e}")
|
|
133
|
+
raise
|
|
134
|
+
finally:
|
|
135
|
+
if scheduler.running:
|
|
136
|
+
scheduler.shutdown()
|
|
137
|
+
logger.info("✅ Scheduler stopped gracefully")
|
|
138
|
+
_scheduler = None # Clear global reference
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Worker Component Development Guide
|
|
2
|
+
|
|
3
|
+
This guide covers arq worker architecture patterns and development for the Aegis Stack worker component.
|
|
4
|
+
|
|
5
|
+
## Worker Architecture (arq)
|
|
6
|
+
|
|
7
|
+
Aegis Stack uses pure **arq patterns** without custom wrappers, following native arq CLI and configuration patterns.
|
|
8
|
+
|
|
9
|
+
### Worker Configuration Structure
|
|
10
|
+
|
|
11
|
+
Each worker queue has its own `WorkerSettings` class:
|
|
12
|
+
- `app/components/worker/queues/system.py` - System maintenance worker
|
|
13
|
+
- `app/components/worker/queues/load_test.py` - Load testing worker
|
|
14
|
+
- `app/components/worker/queues/media.py` - Media processing worker
|
|
15
|
+
|
|
16
|
+
### Worker Services in Docker
|
|
17
|
+
|
|
18
|
+
Workers run as separate Docker services with specific names:
|
|
19
|
+
- **`worker-system`** - System maintenance tasks (low concurrency, high reliability)
|
|
20
|
+
- **`worker-load-test`** - High-concurrency load testing (up to 50 concurrent jobs)
|
|
21
|
+
- **`worker-media`** - File/media processing (commented out by default)
|
|
22
|
+
|
|
23
|
+
## Adding Worker Tasks
|
|
24
|
+
|
|
25
|
+
### 1. Create Task Functions
|
|
26
|
+
Tasks are pure async functions in `app/components/worker/tasks/`:
|
|
27
|
+
```python
|
|
28
|
+
# app/components/worker/tasks/my_tasks.py
|
|
29
|
+
async def my_background_task() -> dict[str, str]:
|
|
30
|
+
"""My custom background task."""
|
|
31
|
+
logger.info("Running my background task")
|
|
32
|
+
|
|
33
|
+
# Your task logic here
|
|
34
|
+
await asyncio.sleep(1) # Simulate work
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
"status": "completed",
|
|
38
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
39
|
+
"task": "my_background_task"
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. Register with Worker Queue
|
|
44
|
+
Import and add to the appropriate `WorkerSettings`:
|
|
45
|
+
```python
|
|
46
|
+
# app/components/worker/queues/system.py
|
|
47
|
+
from app.components.worker.tasks.my_tasks import my_background_task
|
|
48
|
+
|
|
49
|
+
class WorkerSettings:
|
|
50
|
+
functions = [
|
|
51
|
+
system_health_check,
|
|
52
|
+
cleanup_temp_files,
|
|
53
|
+
my_background_task, # Add your task here
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
# Standard arq configuration
|
|
57
|
+
redis_settings = RedisSettings.from_dsn(settings.REDIS_URL)
|
|
58
|
+
queue_name = "arq:queue:system"
|
|
59
|
+
max_jobs = 15
|
|
60
|
+
job_timeout = 300
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Native arq CLI Usage
|
|
64
|
+
|
|
65
|
+
### Worker Health Checks
|
|
66
|
+
```bash
|
|
67
|
+
# Check if workers can connect to Redis and validate configuration
|
|
68
|
+
uv run python -m arq app.components.worker.queues.system.WorkerSettings --check
|
|
69
|
+
uv run python -m arq app.components.worker.queues.load_test.WorkerSettings --check
|
|
70
|
+
uv run python -m arq app.components.worker.queues.media.WorkerSettings --check
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Local Worker Development
|
|
74
|
+
```bash
|
|
75
|
+
# Run worker locally with auto-reload for development
|
|
76
|
+
uv run python -m arq app.components.worker.queues.system.WorkerSettings --watch app/
|
|
77
|
+
|
|
78
|
+
# Run worker in burst mode (process all jobs and exit)
|
|
79
|
+
uv run python -m arq app.components.worker.queues.system.WorkerSettings --burst
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Worker Configuration in Health Checks
|
|
83
|
+
|
|
84
|
+
The health system reads worker configuration from `app/core/config.py` but workers themselves use their own `WorkerSettings` classes:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
# Health system reads this for monitoring
|
|
88
|
+
WORKER_QUEUES: dict[str, dict[str, Any]] = {
|
|
89
|
+
"system": {
|
|
90
|
+
"description": "System maintenance and monitoring tasks",
|
|
91
|
+
"max_jobs": 15,
|
|
92
|
+
"timeout_seconds": 300,
|
|
93
|
+
"queue_name": "arq:queue:system",
|
|
94
|
+
},
|
|
95
|
+
"load_test": {
|
|
96
|
+
"description": "Load testing and performance testing",
|
|
97
|
+
"max_jobs": 50,
|
|
98
|
+
"timeout_seconds": 60,
|
|
99
|
+
"queue_name": "arq:queue:load_test",
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# But workers use their own WorkerSettings classes for actual configuration
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Key Differences from Custom Worker Systems
|
|
107
|
+
|
|
108
|
+
### ✅ What We Do (Pure arq):
|
|
109
|
+
- Use native arq CLI: `python -m arq WorkerSettings`
|
|
110
|
+
- Standard `WorkerSettings` classes with `functions` list
|
|
111
|
+
- Direct task imports into worker configurations
|
|
112
|
+
- Native arq health checking and monitoring
|
|
113
|
+
|
|
114
|
+
### ❌ What We Don't Do (Avoided custom patterns):
|
|
115
|
+
- Custom worker wrapper classes
|
|
116
|
+
- Central worker registry systems
|
|
117
|
+
- Custom CLI commands for workers
|
|
118
|
+
- Configuration-driven task discovery
|
|
119
|
+
|
|
120
|
+
This approach keeps workers transparent and lets developers use arq exactly as documented in the official arq documentation.
|
|
121
|
+
|
|
122
|
+
## Docker Worker Debugging Commands
|
|
123
|
+
|
|
124
|
+
### View Worker Logs
|
|
125
|
+
```bash
|
|
126
|
+
# View specific worker logs
|
|
127
|
+
docker compose logs worker-system # System worker logs
|
|
128
|
+
docker compose logs worker-load-test # Load test worker logs
|
|
129
|
+
docker compose logs -f worker-system # Follow system worker in real-time
|
|
130
|
+
docker compose logs -f worker-load-test # Follow load test worker in real-time
|
|
131
|
+
|
|
132
|
+
# View all workers at once
|
|
133
|
+
docker compose logs -f worker-system worker-load-test
|
|
134
|
+
|
|
135
|
+
# Filter for errors in specific workers
|
|
136
|
+
docker compose logs worker-load-test | grep "ERROR\|failed\|TypeError"
|
|
137
|
+
|
|
138
|
+
# Monitor worker processes and resources
|
|
139
|
+
docker compose exec worker-system ps aux # Check system worker processes
|
|
140
|
+
docker compose exec worker-load-test ps aux # Check load test worker processes
|
|
141
|
+
docker stats worker-system worker-load-test # Monitor resource usage
|
|
142
|
+
docker compose restart worker-system # Restart specific worker
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Essential Docker Log Monitoring
|
|
146
|
+
|
|
147
|
+
**Check Worker Logs for Load Test Issues:**
|
|
148
|
+
```bash
|
|
149
|
+
# View real-time worker logs
|
|
150
|
+
docker compose logs -f worker
|
|
151
|
+
|
|
152
|
+
# Check specific container logs
|
|
153
|
+
docker logs <container-id>
|
|
154
|
+
|
|
155
|
+
# View logs with timestamps
|
|
156
|
+
docker compose logs --timestamps worker
|
|
157
|
+
|
|
158
|
+
# Search logs for specific errors
|
|
159
|
+
docker compose logs worker | grep "TypeError\|failed"
|
|
160
|
+
|
|
161
|
+
# Check all service logs
|
|
162
|
+
docker compose logs -f
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Load Test Debugging Workflow:**
|
|
166
|
+
1. **Run Load Test**: `uv run full-stack load-test run --type io_simulation --tasks 10`
|
|
167
|
+
2. **Monitor Worker Logs**: `docker compose logs -f worker-load-test` (in separate terminal)
|
|
168
|
+
3. **Look for**:
|
|
169
|
+
- `TypeError` messages indicating parameter mismatches
|
|
170
|
+
- Task completion vs failure counts (`j_complete=X j_failed=Y`)
|
|
171
|
+
- Task execution times and errors
|
|
172
|
+
- Redis connection issues
|
|
173
|
+
|
|
174
|
+
**Common Error Patterns:**
|
|
175
|
+
- `TypeError: function() got an unexpected keyword argument 'param'` - Parameter mismatch between orchestrator and task function
|
|
176
|
+
- `j_failed=X` increasing rapidly - Worker tasks failing due to code issues
|
|
177
|
+
- `Redis connection failed` - Infrastructure connectivity problems
|
|
178
|
+
- `delayed=X.XXs` - Queue saturation or worker overload
|
|
179
|
+
|
|
180
|
+
**System Health Verification:**
|
|
181
|
+
```bash
|
|
182
|
+
# Check all containers
|
|
183
|
+
docker compose ps
|
|
184
|
+
|
|
185
|
+
# Check system health via API
|
|
186
|
+
uv run full-stack health status --detailed
|
|
187
|
+
|
|
188
|
+
# Monitor Redis connection
|
|
189
|
+
docker compose logs redis
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Worker Development Best Practices
|
|
193
|
+
|
|
194
|
+
### Task Design Patterns
|
|
195
|
+
1. **Pure Functions** - Tasks should be self-contained with minimal dependencies
|
|
196
|
+
2. **Error Handling** - Always include try/catch with proper logging
|
|
197
|
+
3. **Return Values** - Return structured data for monitoring and debugging
|
|
198
|
+
4. **Timeouts** - Set appropriate timeouts for different task types
|
|
199
|
+
5. **Retry Logic** - Use arq's built-in retry mechanisms
|
|
200
|
+
|
|
201
|
+
### Queue Management
|
|
202
|
+
1. **Separate Concerns** - Use different queues for different types of work
|
|
203
|
+
2. **Concurrency Limits** - Set appropriate max_jobs for each queue type
|
|
204
|
+
3. **Priority Queues** - Use different queues for different priorities
|
|
205
|
+
4. **Dead Letter Queues** - Monitor failed jobs and implement recovery
|
|
206
|
+
|
|
207
|
+
### Monitoring and Observability
|
|
208
|
+
1. **Structured Logging** - Use structured logs for easy parsing
|
|
209
|
+
2. **Metrics Collection** - Track task execution times and success rates
|
|
210
|
+
3. **Health Checks** - Implement health checks for worker availability
|
|
211
|
+
4. **Alerting** - Set up alerts for queue depth and failure rates
|
|
212
|
+
|
|
213
|
+
This approach ensures workers are maintainable, debuggable, and follow established patterns.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Worker component constants.
|
|
3
|
+
|
|
4
|
+
This module contains constants specific to the worker component,
|
|
5
|
+
keeping them separate from global application constants.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TaskNames:
|
|
12
|
+
"""Worker task function names - must match actual function names in code."""
|
|
13
|
+
|
|
14
|
+
# Orchestrator
|
|
15
|
+
LOAD_TEST_ORCHESTRATOR = "load_test_orchestrator"
|
|
16
|
+
|
|
17
|
+
# Load test tasks
|
|
18
|
+
CPU_INTENSIVE_TASK = "cpu_intensive_task"
|
|
19
|
+
IO_SIMULATION_TASK = "io_simulation_task"
|
|
20
|
+
MEMORY_OPERATIONS_TASK = "memory_operations_task"
|
|
21
|
+
FAILURE_TESTING_TASK = "failure_testing_task"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LoadTestTypes(Enum):
|
|
25
|
+
"""Load test type identifiers for task selection."""
|
|
26
|
+
|
|
27
|
+
CPU_INTENSIVE = "cpu_intensive"
|
|
28
|
+
IO_SIMULATION = "io_simulation"
|
|
29
|
+
MEMORY_OPERATIONS = "memory_operations"
|
|
30
|
+
FAILURE_TESTING = "failure_testing"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Worker pool management for client-side task enqueueing.
|
|
3
|
+
|
|
4
|
+
This module provides Redis connection pooling and caching for enqueueing tasks
|
|
5
|
+
to worker queues. Separated from worker management to allow clean architectural
|
|
6
|
+
separation between client-side enqueueing and worker-side processing.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from arq import create_pool
|
|
10
|
+
from arq.connections import ArqRedis, RedisSettings
|
|
11
|
+
|
|
12
|
+
from app.core.config import get_default_queue, settings
|
|
13
|
+
from app.core.log import logger
|
|
14
|
+
|
|
15
|
+
# Global pool cache to avoid creating new Redis connections repeatedly
|
|
16
|
+
_pool_cache: dict[str, ArqRedis] = {}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def get_queue_pool(queue_type: str | None = None) -> tuple[ArqRedis, str]:
|
|
20
|
+
"""
|
|
21
|
+
Get Redis pool for enqueuing tasks to specific functional queue.
|
|
22
|
+
|
|
23
|
+
Uses connection pooling to avoid creating new Redis connections repeatedly.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
queue_type: Functional queue type (defaults to configured default queue)
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Tuple of (pool, queue_name) for enqueueing tasks
|
|
30
|
+
"""
|
|
31
|
+
# Use configured default queue if not specified
|
|
32
|
+
if queue_type is None:
|
|
33
|
+
queue_type = get_default_queue()
|
|
34
|
+
|
|
35
|
+
from app.core.config import is_valid_queue, get_available_queues
|
|
36
|
+
|
|
37
|
+
if not is_valid_queue(queue_type):
|
|
38
|
+
available = get_available_queues()
|
|
39
|
+
raise ValueError(f"Invalid queue type '{queue_type}'. Available: {available}")
|
|
40
|
+
|
|
41
|
+
from app.components.worker.registry import get_queue_metadata
|
|
42
|
+
queue_name = get_queue_metadata(queue_type)["queue_name"]
|
|
43
|
+
|
|
44
|
+
# Check cache first to avoid creating new Redis connections
|
|
45
|
+
cache_key = f"{queue_type}_{settings.REDIS_URL}"
|
|
46
|
+
|
|
47
|
+
if cache_key in _pool_cache:
|
|
48
|
+
# Reuse existing pool
|
|
49
|
+
cached_pool = _pool_cache[cache_key]
|
|
50
|
+
try:
|
|
51
|
+
# Test if pool is still valid by doing a quick ping
|
|
52
|
+
await cached_pool.ping()
|
|
53
|
+
return cached_pool, queue_name
|
|
54
|
+
except Exception:
|
|
55
|
+
# Pool is stale, remove from cache and create new one
|
|
56
|
+
logger.debug(f"Removing stale pool from cache: {cache_key}")
|
|
57
|
+
del _pool_cache[cache_key]
|
|
58
|
+
|
|
59
|
+
# Create new Redis pool and cache it
|
|
60
|
+
redis_settings = RedisSettings.from_dsn(settings.REDIS_URL)
|
|
61
|
+
pool = await create_pool(redis_settings)
|
|
62
|
+
_pool_cache[cache_key] = pool
|
|
63
|
+
|
|
64
|
+
logger.debug(f"Created and cached new Redis pool: {cache_key}")
|
|
65
|
+
return pool, queue_name
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def clear_pool_cache() -> None:
|
|
69
|
+
"""Clear all cached pools. Use during shutdown or for testing."""
|
|
70
|
+
for cache_key, pool in _pool_cache.items():
|
|
71
|
+
try:
|
|
72
|
+
await pool.aclose()
|
|
73
|
+
logger.debug(f"Closed cached pool: {cache_key}")
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.warning(f"Error closing cached pool {cache_key}: {e}")
|
|
76
|
+
|
|
77
|
+
_pool_cache.clear()
|
|
78
|
+
logger.info("Pool cache cleared")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Worker queue configurations using native arq WorkerSettings."""
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Load test worker queue configuration.
|
|
3
|
+
|
|
4
|
+
Handles load testing orchestration and synthetic workload tasks using native arq
|
|
5
|
+
patterns.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from arq.connections import RedisSettings
|
|
9
|
+
|
|
10
|
+
from app.core.config import settings
|
|
11
|
+
|
|
12
|
+
# Import load test tasks
|
|
13
|
+
from app.components.worker.tasks.load_tasks import (
|
|
14
|
+
cpu_intensive_task,
|
|
15
|
+
failure_testing_task,
|
|
16
|
+
io_simulation_task,
|
|
17
|
+
memory_operations_task,
|
|
18
|
+
)
|
|
19
|
+
from app.components.worker.tasks.system_tasks import (
|
|
20
|
+
load_test_orchestrator,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WorkerSettings:
|
|
25
|
+
"""Load testing worker configuration."""
|
|
26
|
+
|
|
27
|
+
# Human-readable description
|
|
28
|
+
description = "Load testing and performance testing"
|
|
29
|
+
|
|
30
|
+
# Task functions for this queue
|
|
31
|
+
functions = [
|
|
32
|
+
# Load test orchestrator
|
|
33
|
+
load_test_orchestrator,
|
|
34
|
+
# Synthetic workload tasks
|
|
35
|
+
cpu_intensive_task,
|
|
36
|
+
io_simulation_task,
|
|
37
|
+
memory_operations_task,
|
|
38
|
+
failure_testing_task,
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
# arq configuration
|
|
42
|
+
redis_settings = RedisSettings.from_dsn(settings.REDIS_URL)
|
|
43
|
+
queue_name = "arq:queue:load_test"
|
|
44
|
+
max_jobs = 50 # High concurrency for load testing
|
|
45
|
+
job_timeout = 60 # Quick tasks
|
|
46
|
+
keep_result = settings.WORKER_KEEP_RESULT_SECONDS
|
|
47
|
+
max_tries = settings.WORKER_MAX_TRIES
|
|
48
|
+
health_check_interval = 30
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Media worker queue configuration.
|
|
3
|
+
|
|
4
|
+
Handles image processing, file operations, and media transformations using native
|
|
5
|
+
arq patterns.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from arq.connections import RedisSettings
|
|
11
|
+
|
|
12
|
+
from app.core.config import settings
|
|
13
|
+
|
|
14
|
+
# Import media tasks (when available)
|
|
15
|
+
# from app.components.worker.tasks.media_tasks import (
|
|
16
|
+
# image_resize,
|
|
17
|
+
# video_encode,
|
|
18
|
+
# file_convert,
|
|
19
|
+
# )
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WorkerSettings:
|
|
23
|
+
"""Media processing worker configuration."""
|
|
24
|
+
|
|
25
|
+
# Human-readable description
|
|
26
|
+
description = "Image and file processing"
|
|
27
|
+
|
|
28
|
+
# Task functions for this queue
|
|
29
|
+
functions: list[Any] = [
|
|
30
|
+
# Media processing tasks will be added here
|
|
31
|
+
# Example: image_resize, video_encode, file_convert
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# arq configuration
|
|
35
|
+
redis_settings = RedisSettings.from_dsn(settings.REDIS_URL)
|
|
36
|
+
queue_name = "arq:queue:media"
|
|
37
|
+
max_jobs = 10 # I/O-bound file operations
|
|
38
|
+
job_timeout = 600 # 10 minutes - file processing can take time
|
|
39
|
+
keep_result = settings.WORKER_KEEP_RESULT_SECONDS
|
|
40
|
+
max_tries = settings.WORKER_MAX_TRIES
|
|
41
|
+
health_check_interval = 30
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
System worker queue configuration.
|
|
3
|
+
|
|
4
|
+
Handles system maintenance and monitoring tasks using native arq patterns.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from arq.connections import RedisSettings
|
|
8
|
+
|
|
9
|
+
from app.core.config import settings
|
|
10
|
+
|
|
11
|
+
# Import system tasks
|
|
12
|
+
from app.components.worker.tasks.simple_system_tasks import (
|
|
13
|
+
system_health_check,
|
|
14
|
+
cleanup_temp_files,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
class WorkerSettings:
|
|
18
|
+
"""System maintenance worker configuration."""
|
|
19
|
+
|
|
20
|
+
# Human-readable description
|
|
21
|
+
description = "System maintenance and monitoring tasks"
|
|
22
|
+
|
|
23
|
+
# Task functions for this queue
|
|
24
|
+
functions = [
|
|
25
|
+
system_health_check,
|
|
26
|
+
cleanup_temp_files,
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# arq configuration
|
|
30
|
+
redis_settings = RedisSettings.from_dsn(settings.REDIS_URL)
|
|
31
|
+
queue_name = "arq:queue:system"
|
|
32
|
+
max_jobs = 15 # Moderate concurrency for administrative operations
|
|
33
|
+
job_timeout = 300 # 5 minutes
|
|
34
|
+
keep_result = settings.WORKER_KEEP_RESULT_SECONDS
|
|
35
|
+
max_tries = settings.WORKER_MAX_TRIES
|
|
36
|
+
health_check_interval = 30
|