diff --git a/services/scheduler/TASK_REGISTRATION.md b/services/scheduler/TASK_REGISTRATION.md new file mode 100644 index 0000000..af3c372 --- /dev/null +++ b/services/scheduler/TASK_REGISTRATION.md @@ -0,0 +1,289 @@ +# Scheduler Task Registration Guide + +## Overview + +The Scheduler is **DB-driven** (not YAML-based). Tasks are registered via REST API and stored in PostgreSQL. + +## Quick Reference + +### Register a New Task + +```bash +curl -X POST http://192.168.86.149:8090/tasks \ + -H "Authorization: Bearer $SCHEDULER_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "task_name": "my_task", + "service": "my-service", + "executor": "rest_api", + "priority": 25, + "description": "Task description", + "minute": 0, + "hour": -1, + "config": { + "method": "POST", + "url": "http://service:port/endpoint", + "headers": { + "Authorization": "Bearer ${ENV_VAR}" + }, + "body": {} + } + }' +``` + +### List All Tasks + +```bash +curl http://192.168.86.149:8090/tasks \ + -H "Authorization: Bearer $SCHEDULER_API_KEY" +``` + +### Trigger Task Manually + +```bash +curl -X POST http://192.168.86.149:8090/tasks/{task_name}/trigger \ + -H "Authorization: Bearer $SCHEDULER_API_KEY" +``` + +### View Task Execution History + +```bash +curl http://192.168.86.149:8090/executions?task_name={task_name} \ + -H "Authorization: Bearer $SCHEDULER_API_KEY" +``` + +## Schedule Patterns + +Use cron-style fields where **`-1` means "every"**: + +| Pattern | minute | hour | day_of_month | month | day_of_week | Description | +|---------|--------|------|--------------|-------|-------------|-------------| +| Every minute | -1 | -1 | -1 | -1 | -1 | Runs every minute | +| Hourly | 0 | -1 | -1 | -1 | -1 | Runs at minute 0 of every hour | +| Daily at 2 AM | 0 | 2 | -1 | -1 | -1 | Runs at 2:00 AM every day | +| Weekly (Monday 2 AM) | 0 | 2 | -1 | -1 | 0 | Runs at 2:00 AM every Monday | +| Monthly (1st at 2 AM) | 0 | 2 | 1 | -1 | -1 | Runs at 2:00 AM on 1st of month | + +**Note:** `day_of_week` is 0-6 (Monday-Sunday) + +## Priority Levels + +| Priority Range | Level | Use Cases | +|----------------|-------|-----------| +| 1-5 | Emergency/System | Critical system tasks | +| 10-30 | User | User-initiated tasks | +| 40-70 | Maintenance | Background maintenance | +| 70+ | Low | Low priority cleanup | + +## Executor Types + +### `rest_api` Executor + +Calls HTTP endpoints. Supports environment variable substitution in headers/body. + +**Config Format:** +```json +{ + "method": "POST|GET|PUT|DELETE|PATCH", + "url": "http://service:port/endpoint", + "headers": { + "Authorization": "Bearer ${ENV_VAR}", + "Content-Type": "application/json" + }, + "body": { + "key": "value" + }, + "timeout": 30, + "auth": { + "type": "bearer|basic|api_key", + "value": "${TOKEN_ENV_VAR}" + } +} +``` + +**Environment Variables:** Use `${VAR_NAME}` syntax to reference env vars from the scheduler container. + +### Other Executors + +- `shell`: Execute shell commands +- `python`: Execute Python scripts +- `docker`: Docker operations +- `backup`: Backup operations +- `doc_sync`: Documentation sync + +## Complete Task Schema + +### Required Fields + +- `task_name` (string): Unique task identifier +- `service` (string): Service that owns this task (e.g., "library-desk", "core-api") +- `executor` (string): Executor type ("rest_api", "shell", etc.) +- `priority` (int): Priority level (1-100) + +### Optional Fields + +- `description` (string): Human-readable description +- `minute` (int): -1 to 59, default: -1 (every minute) +- `hour` (int): -1 to 23, default: -1 (every hour) +- `day_of_month` (int): -1 to 31, default: -1 (every day) +- `month` (int): -1 to 12, default: -1 (every month) +- `day_of_week` (int): -1 to 6, default: -1 (every day) +- `enabled` (bool): Whether task is enabled, default: true +- `max_retries` (int): Max retry attempts, default: 3 +- `timeout_seconds` (int): Execution timeout, default: 3600 +- `config` (object): Executor-specific configuration +- `created_by` (string): Creator identifier, default: "api" + +## Example: Librarian Knowledge Consolidation + +This task processes unprocessed search queries and consolidates knowledge into wiki pages. + +**Registration:** +```bash +curl -X POST http://192.168.86.149:8090/tasks \ + -H "Authorization: Bearer $SCHEDULER_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "task_name": "librarian_consolidation", + "service": "library-desk", + "executor": "rest_api", + "priority": 25, + "description": "Processes unprocessed search queries and consolidates knowledge", + "minute": 0, + "hour": -1, + "config": { + "method": "POST", + "url": "http://library-desk:8089/consolidation/run", + "headers": { + "Authorization": "Bearer ${LIBRARY_API_KEY}", + "Content-Type": "application/json" + }, + "body": { + "dry_run": false, + "process_limit": 10, + "lookback_days": 7, + "min_web_results": 2 + } + } + }' +``` + +**Schedule:** Runs hourly (at minute 0 of every hour) + +**What it does:** +1. Queries Neo4j for unprocessed `SearchQuery` nodes +2. Analyzes web results with Ollama +3. Creates/updates wiki pages via WikiPageWriter +4. Extracts and adds new entities to knowledge graph +5. Marks searches as processed + +## API Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/tasks` | List all tasks | +| GET | `/tasks/{task_name}` | Get task details | +| POST | `/tasks` | Create new task | +| PUT | `/tasks/{task_name}` | Update task | +| DELETE | `/tasks/{task_name}` | Delete task | +| POST | `/tasks/{task_name}/trigger` | Manually trigger task | +| GET | `/executions` | View execution history | +| GET | `/stats` | Get scheduler statistics | +| GET | `/health` | Health check | + +## Interactive Documentation + +Visit **http://192.168.86.149:8090/docs** for interactive Swagger UI with: +- Full API documentation +- Request/response schemas +- Try-it-out functionality +- Example payloads + +## Environment Variables + +Get API keys from containers: +```bash +# Scheduler API key +docker exec scheduler printenv SCHEDULER_API_KEY + +# Library Desk API key +docker exec library-desk printenv LIBRARY_API_KEY +``` + +## Task Management Tips + +1. **Test with dry_run**: Set `enabled: false` initially or use dry_run in task config +2. **Monitor executions**: Check `/executions` endpoint regularly +3. **Start conservative**: Use longer intervals (hourly, daily) before going to frequent runs +4. **Use priorities wisely**: Reserve 1-10 for critical tasks, use 20-30 for user tasks +5. **Set timeouts appropriately**: Long-running tasks need higher timeout_seconds + +## Troubleshooting + +### Task not running + +1. Check if task is enabled: `GET /tasks/{task_name}` +2. Check execution history: `GET /executions?task_name={task_name}` +3. Verify schedule matches current time +4. Check scheduler logs: `docker logs scheduler` + +### Task failing + +1. Check execution history for error messages +2. Verify endpoint is accessible from scheduler container +3. Test endpoint manually with curl +4. Check API keys and authentication +5. Verify executor configuration (especially for rest_api) + +### Debug a task + +```bash +# Get task details +curl http://192.168.86.149:8090/tasks/librarian_consolidation \ + -H "Authorization: Bearer $SCHEDULER_API_KEY" + +# View recent executions +curl http://192.168.86.149:8090/executions?task_name=librarian_consolidation&limit=5 \ + -H "Authorization: Bearer $SCHEDULER_API_KEY" + +# Trigger manually +curl -X POST http://192.168.86.149:8090/tasks/librarian_consolidation/trigger \ + -H "Authorization: Bearer $SCHEDULER_API_KEY" +``` + +## Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ The Scheduler (AsyncIOScheduler) │ +│ │ +│ ┌────────────────────────────────────────────┐ │ +│ │ Cron Job (every minute) │ │ +│ │ ↓ │ │ +│ │ Query PostgreSQL for tasks scheduled │ │ +│ │ for current minute/hour/day │ │ +│ │ ↓ │ │ +│ │ Execute up to 5 tasks concurrently │ │ +│ │ (based on priority) │ │ +│ │ ↓ │ │ +│ │ Call appropriate executor: │ │ +│ │ - rest_api → HTTP request │ │ +│ │ - shell → subprocess │ │ +│ │ - python → exec │ │ +│ └────────────────────────────────────────────┘ │ +│ │ +│ PostgreSQL: │ +│ - scheduled_tasks (task definitions) │ +│ - task_executions (execution history) │ +└─────────────────────────────────────────────────────────┘ +``` + +## Current Registered Tasks + +As of 2025-12-09: + +1. **backup_docker_configs_daily** - Daily at 3:05 AM +2. **librarian_consolidation** - Hourly (minute 0) +3. **test_example_task** - Every minute (testing) +4. **sync_fastapi_docs_monthly** - 11th of month at 4:00 AM +5. **sync_ollama_docs_monthly** - 12th of month at 4:00 AM diff --git a/services/scheduler/src/main.py b/services/scheduler/src/main.py index 5388c90..086c888 100644 --- a/services/scheduler/src/main.py +++ b/services/scheduler/src/main.py @@ -16,6 +16,7 @@ import logging from src.config import get_settings, Settings from src.tasks.executor import TaskExecutor +from src.models import TaskCreate, TaskUpdate, TaskResponse # Configure logging logging.basicConfig( @@ -194,29 +195,82 @@ async def get_task_details( return dict(task) -@app.post("/tasks") +@app.post("/tasks", response_model=TaskResponse, tags=["Task Management"]) async def create_task( - task_data: dict, + task: TaskCreate, api_key: str = Depends(verify_api_key), executor: TaskExecutor = Depends(get_task_executor) ): - """Create a new scheduled task.""" + """ + Create a new scheduled task. + + ## Schedule Pattern + + Use cron-style fields where `-1` means "every": + - `minute: -1, hour: -1` → Runs every minute + - `minute: 0, hour: -1` → Runs at minute 0 of every hour (hourly) + - `minute: 0, hour: 2` → Runs at 2:00 AM every day + - `minute: 0, hour: 2, day_of_week: 0` → Runs at 2:00 AM every Monday + + ## Priority Levels + + - **1-5**: Emergency/critical system tasks + - **10-30**: User-initiated tasks + - **40-70**: Background maintenance + - **70+**: Low priority cleanup + + ## Executor Types + + ### `rest_api` Executor + + Calls HTTP endpoints. Config format: + ```json + { + "method": "POST", + "url": "http://service:port/endpoint", + "headers": { + "Authorization": "Bearer ${ENV_VAR}", + "Content-Type": "application/json" + }, + "body": {"key": "value"} + } + ``` + + Environment variables can be referenced with `${VAR_NAME}` syntax. + + ## Example: Librarian Consolidation Task + + ```json + { + "task_name": "librarian_consolidation", + "service": "library-desk", + "executor": "rest_api", + "priority": 25, + "description": "Processes search queries and consolidates knowledge", + "minute": 0, + "hour": -1, + "config": { + "method": "POST", + "url": "http://library-desk:8089/consolidation/run", + "headers": { + "Authorization": "Bearer ${LIBRARY_DESK_API_KEY}", + "Content-Type": "application/json" + }, + "body": { + "dry_run": false, + "process_limit": 10 + } + } + } + ``` + + This creates an hourly task that calls the consolidation endpoint. + """ import psycopg2.extras import json - required_fields = ['task_name', 'service', 'executor', 'priority'] - if not all(field in task_data for field in required_fields): - raise HTTPException(400, f"Missing required fields: {required_fields}") - - # Set defaults - task_data.setdefault('minute', -1) - task_data.setdefault('hour', -1) - task_data.setdefault('day_of_month', -1) - task_data.setdefault('month', -1) - task_data.setdefault('day_of_week', -1) - task_data.setdefault('enabled', True) - task_data.setdefault('max_retries', 3) - task_data.setdefault('timeout_seconds', 3600) + # Convert task model to dict + task_data = task.model_dump() # Convert config dict to JSON string if present if 'config' in task_data and isinstance(task_data['config'], dict): @@ -236,7 +290,7 @@ async def create_task( %(config)s::jsonb, %(max_retries)s, %(timeout_seconds)s, %(created_by)s) RETURNING * - """, {**task_data, 'created_by': task_data.get('created_by', 'api')}) + """, task_data) new_task = dict(cur.fetchone()) conn.commit() diff --git a/services/scheduler/src/models.py b/services/scheduler/src/models.py new file mode 100644 index 0000000..f131b7f --- /dev/null +++ b/services/scheduler/src/models.py @@ -0,0 +1,210 @@ +""" +Pydantic models for The Scheduler API. +""" +from pydantic import BaseModel, Field +from typing import Optional, Dict, Any +from enum import Enum + + +class SchedulePattern(str, Enum): + """Common schedule patterns.""" + EVERY_MINUTE = "every_minute" + HOURLY = "hourly" + DAILY = "daily" + WEEKLY = "weekly" + MONTHLY = "monthly" + + +class TaskExecutor(str, Enum): + """Available task executors.""" + REST_API = "rest_api" + SHELL = "shell" + PYTHON = "python" + + +class TaskPriorityLevel(str, Enum): + """Task priority levels.""" + EMERGENCY = "emergency" # 1-5: Critical system tasks + SYSTEM = "system" # 5-10: System maintenance + USER = "user" # 10-30: User-initiated tasks + MAINTENANCE = "maintenance" # 40-70: Background maintenance + LOW = "low" # 70+: Low priority cleanup + + +class TaskBase(BaseModel): + """Base task fields.""" + task_name: str = Field( + ..., + description="Unique task identifier (e.g., 'librarian_consolidation')", + example="librarian_consolidation" + ) + service: str = Field( + ..., + description="Service that owns this task (e.g., 'library-desk', 'core-api')", + example="library-desk" + ) + executor: str = Field( + ..., + description="Executor type: 'rest_api', 'shell', or 'python'", + example="rest_api" + ) + priority: int = Field( + ..., + ge=1, + le=100, + description="Priority level (1-5: emergency, 10-30: user, 40-70: maintenance, 70+: low)", + example=25 + ) + description: Optional[str] = Field( + None, + description="Human-readable task description", + example="Processes unprocessed search queries and consolidates knowledge into wiki pages" + ) + + +class TaskSchedule(BaseModel): + """Cron-style schedule fields.""" + minute: int = Field( + -1, + ge=-1, + le=59, + description="Minute to run (-1 = every minute, 0-59 = specific minute)", + example=-1 + ) + hour: int = Field( + -1, + ge=-1, + le=23, + description="Hour to run (-1 = every hour, 0-23 = specific hour)", + example=-1 + ) + day_of_month: int = Field( + -1, + ge=-1, + le=31, + description="Day of month to run (-1 = every day, 1-31 = specific day)", + example=-1 + ) + month: int = Field( + -1, + ge=-1, + le=12, + description="Month to run (-1 = every month, 1-12 = specific month)", + example=-1 + ) + day_of_week: int = Field( + -1, + ge=-1, + le=6, + description="Day of week to run (-1 = every day, 0-6 = Monday-Sunday)", + example=-1 + ) + + +class TaskConfig(BaseModel): + """Task execution configuration.""" + enabled: bool = Field( + True, + description="Whether task is enabled", + example=True + ) + max_retries: int = Field( + 3, + ge=0, + le=10, + description="Maximum retry attempts on failure", + example=3 + ) + timeout_seconds: int = Field( + 3600, + ge=1, + description="Execution timeout in seconds", + example=3600 + ) + config: Optional[Dict[str, Any]] = Field( + None, + description="Executor-specific configuration (varies by executor type)", + example={ + "method": "POST", + "url": "http://library-desk:8089/consolidation/run", + "headers": { + "Authorization": "Bearer ${LIBRARY_DESK_API_KEY}", + "Content-Type": "application/json" + }, + "body": { + "dry_run": False, + "process_limit": 10, + "lookback_days": 7, + "min_web_results": 2 + } + } + ) + + +class TaskCreate(TaskBase, TaskSchedule, TaskConfig): + """Request model for creating a new scheduled task.""" + created_by: Optional[str] = Field( + "api", + description="User or system that created this task", + example="api" + ) + + class Config: + json_schema_extra = { + "example": { + "task_name": "librarian_consolidation", + "service": "library-desk", + "executor": "rest_api", + "priority": 25, + "description": "Processes unprocessed search queries and consolidates knowledge", + "minute": 0, + "hour": -1, + "day_of_month": -1, + "month": -1, + "day_of_week": -1, + "enabled": True, + "max_retries": 3, + "timeout_seconds": 3600, + "config": { + "method": "POST", + "url": "http://library-desk:8089/consolidation/run", + "headers": { + "Authorization": "Bearer ${LIBRARY_DESK_API_KEY}", + "Content-Type": "application/json" + }, + "body": { + "dry_run": False, + "process_limit": 10, + "lookback_days": 7, + "min_web_results": 2 + } + }, + "created_by": "api" + } + } + + +class TaskUpdate(BaseModel): + """Request model for updating a scheduled task.""" + service: Optional[str] = None + executor: Optional[str] = None + priority: Optional[int] = Field(None, ge=1, le=100) + minute: Optional[int] = Field(None, ge=-1, le=59) + hour: Optional[int] = Field(None, ge=-1, le=23) + day_of_month: Optional[int] = Field(None, ge=-1, le=31) + month: Optional[int] = Field(None, ge=-1, le=12) + day_of_week: Optional[int] = Field(None, ge=-1, le=6) + enabled: Optional[bool] = None + description: Optional[str] = None + config: Optional[Dict[str, Any]] = None + max_retries: Optional[int] = Field(None, ge=0, le=10) + timeout_seconds: Optional[int] = Field(None, ge=1) + + +class TaskResponse(TaskCreate): + """Response model for task operations.""" + created_at: str + updated_at: Optional[str] = None + + class Config: + from_attributes = True