Files
bakery-ia/docs/PRODUCTION_PLANNING_SYSTEM.md
2025-10-09 18:01:24 +02:00

719 lines
23 KiB
Markdown

# Production Planning System Documentation
## Overview
The Production Planning System automates daily production and procurement scheduling for bakery operations. The system consists of two primary schedulers that run every morning to generate plans based on demand forecasts, inventory levels, and capacity constraints.
**Last Updated:** 2025-10-09
**Version:** 2.0 (Automated Scheduling)
**Status:** Production Ready
---
## Architecture
### System Components
```
┌─────────────────────────────────────────────────────────────────┐
│ DAILY PLANNING WORKFLOW │
└─────────────────────────────────────────────────────────────────┘
05:30 AM → Production Scheduler
├─ Generates production schedules for all tenants
├─ Calls Forecasting Service (cached) for demand
├─ Calls Orders Service for demand requirements
├─ Creates production batches
└─ Sends notifications to production managers
06:00 AM → Procurement Scheduler
├─ Generates procurement plans for all tenants
├─ Calls Forecasting Service (cached - reuses cached data!)
├─ Calls Inventory Service for stock levels
├─ Matches suppliers for requirements
└─ Sends notifications to procurement managers
08:00 AM → Operators review plans
├─ Accept → Plans move to "approved" status
├─ Reject → Automatic regeneration if stale data detected
└─ Modify → Recalculate and resubmit
Throughout Day → Alert services monitor execution
├─ Production delays
├─ Capacity issues
├─ Quality problems
└─ Equipment failures
```
### Services Involved
| Service | Role | Endpoints |
|---------|------|-----------|
| **Production Service** | Generates daily production schedules | `POST /api/v1/{tenant_id}/production/operations/schedule` |
| **Orders Service** | Generates daily procurement plans | `POST /api/v1/{tenant_id}/orders/operations/procurement/generate` |
| **Forecasting Service** | Provides demand predictions (cached) | `POST /api/v1/{tenant_id}/forecasting/operations/single` |
| **Inventory Service** | Provides current stock levels | `GET /api/v1/{tenant_id}/inventory/products` |
| **Tenant Service** | Provides timezone configuration | `GET /api/v1/tenants/{tenant_id}` |
---
## Schedulers
### 1. Production Scheduler
**Service:** Production Service
**Class:** `ProductionSchedulerService`
**File:** [`services/production/app/services/production_scheduler_service.py`](../services/production/app/services/production_scheduler_service.py)
#### Schedule
| Job | Time | Purpose | Grace Period |
|-----|------|---------|--------------|
| **Daily Production Planning** | 5:30 AM (tenant timezone) | Generate next-day production schedules | 5 minutes |
| **Stale Schedule Cleanup** | 5:50 AM | Archive/cancel old schedules, send escalations | 5 minutes |
| **Test Mode** | Every 30 min (DEBUG only) | Development/testing | 5 minutes |
#### Features
-**Timezone-aware**: Respects tenant timezone configuration
-**Leader election**: Only one instance runs in distributed deployment
-**Idempotent**: Checks if schedule exists before creating
-**Parallel processing**: Processes tenants concurrently with timeouts
-**Error isolation**: Tenant failures don't affect others
-**Demo tenant filtering**: Excludes demo tenants from automation
#### Workflow
1. **Tenant Discovery**: Fetch all active non-demo tenants
2. **Parallel Processing**: Process each tenant concurrently (180s timeout)
3. **Date Calculation**: Use tenant timezone to determine target date
4. **Duplicate Check**: Skip if schedule already exists
5. **Requirements Calculation**: Call `calculate_daily_requirements()`
6. **Schedule Creation**: Create schedule with status "draft"
7. **Batch Generation**: Create production batches from requirements
8. **Notification**: Send alert to production managers
9. **Monitoring**: Record metrics for observability
#### Configuration
```python
# Environment Variables
PRODUCTION_TEST_MODE=false # Enable 30-minute test job
DEBUG=false # Enable verbose logging
# Tenant Configuration
tenant.timezone=Europe/Madrid # IANA timezone string
```
---
### 2. Procurement Scheduler
**Service:** Orders Service
**Class:** `ProcurementSchedulerService`
**File:** [`services/orders/app/services/procurement_scheduler_service.py`](../services/orders/app/services/procurement_scheduler_service.py)
#### Schedule
| Job | Time | Purpose | Grace Period |
|-----|------|---------|--------------|
| **Daily Procurement Planning** | 6:00 AM (tenant timezone) | Generate next-day procurement plans | 5 minutes |
| **Stale Plan Cleanup** | 6:30 AM | Archive/cancel old plans, send reminders | 5 minutes |
| **Weekly Optimization** | Monday 7:00 AM | Weekly procurement optimization review | 10 minutes |
| **Test Mode** | Every 30 min (DEBUG only) | Development/testing | 5 minutes |
#### Features
-**Timezone-aware**: Respects tenant timezone configuration
-**Leader election**: Prevents duplicate runs
-**Idempotent**: Checks if plan exists before generating
-**Parallel processing**: 120s timeout per tenant
-**Forecast fallback**: Uses historical data if forecast unavailable
-**Critical stock alerts**: Automatic alerts for zero-stock items
-**Rejection workflow**: Auto-regeneration for rejected plans
#### Workflow
1. **Tenant Discovery**: Fetch active non-demo tenants
2. **Parallel Processing**: Process each tenant (120s timeout)
3. **Date Calculation**: Use tenant timezone
4. **Duplicate Check**: Skip if plan exists (unless force_regenerate)
5. **Forecasting**: Call Forecasting Service (uses cache!)
6. **Inventory Check**: Get current stock levels
7. **Requirements Calculation**: Calculate net requirements
8. **Supplier Matching**: Find suitable suppliers
9. **Plan Creation**: Create plan with status "draft"
10. **Critical Alerts**: Send alerts for critical items
11. **Notification**: Notify procurement managers
12. **Caching**: Cache plan in Redis (6h TTL)
---
## Forecast Caching
### Overview
To eliminate redundant forecast computations, the Forecasting Service now includes a service-level Redis cache. Both Production and Procurement schedulers benefit from this without any code changes.
**File:** [`services/forecasting/app/services/forecast_cache.py`](../services/forecasting/app/services/forecast_cache.py)
### Cache Strategy
```
Key Format: forecast:{tenant_id}:{product_id}:{forecast_date}
TTL: Until midnight of day after forecast_date
Example: forecast:abc-123:prod-456:2025-10-10 → expires 2025-10-11 00:00:00
```
### Cache Flow
```
Client Request → Forecasting API
Check Redis Cache
├─ HIT → Return cached result (add 'cached: true')
└─ MISS → Generate forecast
Cache result (TTL)
Return result
```
### Benefits
| Metric | Before Caching | After Caching | Improvement |
|--------|---------------|---------------|-------------|
| **Duplicate Forecasts** | 2x per day (Production + Procurement) | 1x per day | 50% reduction |
| **Forecast Response Time** | ~2-5 seconds | ~50-100ms (cache hit) | 95%+ faster |
| **Forecasting Service Load** | 100% | 50% | 50% reduction |
| **Cache Hit Rate** | N/A | ~80-90% (expected) | - |
### Cache Invalidation
Forecasts are invalidated when:
1. **TTL Expiry**: Automatic at midnight after forecast_date
2. **Model Retraining**: When ML model is retrained for product
3. **Manual Invalidation**: Via API endpoint (admin only)
```python
# Invalidate specific product forecasts
DELETE /api/v1/{tenant_id}/forecasting/cache/product/{product_id}
# Invalidate all tenant forecasts
DELETE /api/v1/{tenant_id}/forecasting/cache
# Invalidate all forecasts (use with caution!)
DELETE /admin/forecasting/cache/all
```
---
## Plan Rejection Workflow
### Overview
When a procurement plan is rejected by an operator, the system automatically handles the rejection with notifications and optional regeneration.
**File:** [`services/orders/app/services/procurement_service.py`](../services/orders/app/services/procurement_service.py:1244-1404)
### Rejection Flow
```
User Rejects Plan (status → "cancelled")
Record rejection in approval_workflow (JSONB)
Send notification to stakeholders
Publish rejection event (RabbitMQ)
Analyze rejection reason
├─ Contains "stale", "outdated", etc. → Auto-regenerate
└─ Other reason → Manual regeneration required
Schedule regeneration (if applicable)
Send regeneration request event
```
### Auto-Regeneration Keywords
Plans are automatically regenerated if rejection notes contain:
- `stale`
- `outdated`
- `old data`
- `datos antiguos` (Spanish)
- `desactualizado` (Spanish)
- `obsoleto` (Spanish)
### Events Published
| Event | Routing Key | Consumers |
|-------|-------------|-----------|
| **Plan Rejected** | `procurement.plan.rejected` | Alert Service, UI Notifications |
| **Regeneration Requested** | `procurement.plan.regeneration_requested` | Procurement Scheduler |
| **Plan Status Changed** | `procurement.plan.status_changed` | Inventory Service, Dashboard |
---
## Timezone Configuration
### Overview
All schedulers are timezone-aware to ensure accurate "daily" execution relative to the bakery's local time.
### Tenant Configuration
**Model:** `Tenant`
**File:** [`services/tenant/app/models/tenants.py`](../services/tenant/app/models/tenants.py:32-33)
**Field:** `timezone` (String, default: `"Europe/Madrid"`)
**Migration:** [`services/tenant/migrations/versions/20251009_add_timezone_to_tenants.py`](../services/tenant/migrations/versions/20251009_add_timezone_to_tenants.py)
### Supported Timezones
All IANA timezone strings are supported. Common examples:
- `Europe/Madrid` (Spain - CEST/CET)
- `Europe/London` (UK - BST/GMT)
- `America/New_York` (US Eastern)
- `America/Los_Angeles` (US Pacific)
- `Asia/Tokyo` (Japan)
- `UTC` (Universal Time)
### Usage in Schedulers
```python
from shared.utils.timezone_helper import TimezoneHelper
# Get current date in tenant's timezone
target_date = TimezoneHelper.get_current_date_in_timezone(tenant_tz)
# Get current datetime in tenant's timezone
now = TimezoneHelper.get_current_datetime_in_timezone(tenant_tz)
# Check if within business hours
is_business_hours = TimezoneHelper.is_business_hours(
timezone_str=tenant_tz,
start_hour=8,
end_hour=20
)
```
---
## Monitoring & Alerts
### Prometheus Metrics
**File:** [`shared/monitoring/scheduler_metrics.py`](../shared/monitoring/scheduler_metrics.py)
#### Key Metrics
| Metric | Type | Description |
|--------|------|-------------|
| `production_schedules_generated_total` | Counter | Total production schedules generated (by tenant, status) |
| `production_schedule_generation_duration_seconds` | Histogram | Time to generate schedule per tenant |
| `procurement_plans_generated_total` | Counter | Total procurement plans generated (by tenant, status) |
| `procurement_plan_generation_duration_seconds` | Histogram | Time to generate plan per tenant |
| `forecast_cache_hits_total` | Counter | Forecast cache hits (by tenant) |
| `forecast_cache_misses_total` | Counter | Forecast cache misses (by tenant) |
| `forecast_cache_hit_rate` | Gauge | Cache hit rate percentage (0-100) |
| `procurement_plan_rejections_total` | Counter | Plan rejections (by tenant, auto_regenerated) |
| `scheduler_health_status` | Gauge | Scheduler health (1=healthy, 0=unhealthy) |
| `tenant_processing_timeout_total` | Counter | Tenant processing timeouts (by service) |
### Recommended Alerts
```yaml
# Alert: Daily production planning failed
- alert: DailyProductionPlanningFailed
expr: production_schedules_generated_total{status="failure"} > 0
for: 10m
labels:
severity: high
annotations:
summary: "Daily production planning failed for at least one tenant"
description: "Check production scheduler logs for tenant {{ $labels.tenant_id }}"
# Alert: Daily procurement planning failed
- alert: DailyProcurementPlanningFailed
expr: procurement_plans_generated_total{status="failure"} > 0
for: 10m
labels:
severity: high
annotations:
summary: "Daily procurement planning failed for at least one tenant"
description: "Check procurement scheduler logs for tenant {{ $labels.tenant_id }}"
# Alert: No production schedules in 24 hours
- alert: NoProductionSchedulesGenerated
expr: rate(production_schedules_generated_total{status="success"}[24h]) == 0
for: 1h
labels:
severity: critical
annotations:
summary: "No production schedules generated in last 24 hours"
description: "Production scheduler may be down or misconfigured"
# Alert: Forecast cache hit rate low
- alert: ForecastCacheHitRateLow
expr: forecast_cache_hit_rate < 50
for: 30m
labels:
severity: warning
annotations:
summary: "Forecast cache hit rate below 50%"
description: "Cache may not be functioning correctly for tenant {{ $labels.tenant_id }}"
# Alert: High tenant processing timeouts
- alert: HighTenantProcessingTimeouts
expr: rate(tenant_processing_timeout_total[5m]) > 0.1
for: 15m
labels:
severity: warning
annotations:
summary: "High rate of tenant processing timeouts"
description: "{{ $labels.service }} scheduler experiencing timeouts for tenant {{ $labels.tenant_id }}"
# Alert: Scheduler unhealthy
- alert: SchedulerUnhealthy
expr: scheduler_health_status == 0
for: 5m
labels:
severity: critical
annotations:
summary: "Scheduler is unhealthy"
description: "{{ $labels.service }} {{ $labels.scheduler_type }} scheduler is reporting unhealthy status"
```
### Grafana Dashboard
Create dashboard with panels for:
1. **Scheduler Success Rate** (line chart)
- `production_schedules_generated_total{status="success"}`
- `procurement_plans_generated_total{status="success"}`
2. **Schedule Generation Duration** (heatmap)
- `production_schedule_generation_duration_seconds`
- `procurement_plan_generation_duration_seconds`
3. **Forecast Cache Hit Rate** (gauge)
- `forecast_cache_hit_rate`
4. **Tenant Processing Status** (pie chart)
- `production_tenants_processed_total`
- `procurement_tenants_processed_total`
5. **Plan Rejections** (table)
- `procurement_plan_rejections_total`
6. **Scheduler Health** (status panel)
- `scheduler_health_status`
---
## Testing
### Manual Testing
#### Test Production Scheduler
```bash
# Trigger test production schedule generation
curl -X POST http://production-service:8000/test/production-scheduler \
-H "Authorization: Bearer $TOKEN"
# Expected response:
{
"message": "Production scheduler test triggered successfully"
}
```
#### Test Procurement Scheduler
```bash
# Trigger test procurement plan generation
curl -X POST http://orders-service:8000/test/procurement-scheduler \
-H "Authorization: Bearer $TOKEN"
# Expected response:
{
"message": "Procurement scheduler test triggered successfully"
}
```
### Automated Testing
```python
# Test production scheduler
async def test_production_scheduler():
scheduler = ProductionSchedulerService(config)
await scheduler.start()
await scheduler.test_production_schedule_generation()
assert scheduler._checks_performed > 0
# Test procurement scheduler
async def test_procurement_scheduler():
scheduler = ProcurementSchedulerService(config)
await scheduler.start()
await scheduler.test_procurement_generation()
assert scheduler._checks_performed > 0
# Test forecast caching
async def test_forecast_cache():
cache = get_forecast_cache_service(redis_url)
# Cache forecast
await cache.cache_forecast(tenant_id, product_id, forecast_date, data)
# Retrieve cached forecast
cached = await cache.get_cached_forecast(tenant_id, product_id, forecast_date)
assert cached is not None
assert cached['cached'] == True
```
---
## Troubleshooting
### Scheduler Not Running
**Symptoms:** No schedules/plans generated in morning
**Checks:**
1. Verify scheduler service is running: `kubectl get pods -n production`
2. Check scheduler health endpoint: `curl http://service:8000/health`
3. Check APScheduler status in logs: `grep "scheduler" logs/production.log`
4. Verify leader election (distributed setup): Check `is_leader` in logs
**Solutions:**
- Restart service: `kubectl rollout restart deployment/production-service`
- Check environment variables: `PRODUCTION_TEST_MODE`, `DEBUG`
- Verify database connectivity
- Check RabbitMQ connectivity for leader election
### Timezone Issues
**Symptoms:** Schedules generated at wrong time
**Checks:**
1. Check tenant timezone configuration:
```sql
SELECT id, name, timezone FROM tenants WHERE id = '{tenant_id}';
```
2. Verify server timezone: `date` (should be UTC in containers)
3. Check logs for timezone warnings
**Solutions:**
- Update tenant timezone: `UPDATE tenants SET timezone = 'Europe/Madrid' WHERE id = '{tenant_id}';`
- Verify TimezoneHelper is being used in schedulers
- Check cron trigger configuration uses correct timezone
### Low Cache Hit Rate
**Symptoms:** `forecast_cache_hit_rate < 50%`
**Checks:**
1. Verify Redis is running: `redis-cli ping`
2. Check cache keys: `redis-cli KEYS "forecast:*"`
3. Check TTL on cache entries: `redis-cli TTL "forecast:{tenant}:{product}:{date}"`
4. Review logs for cache errors
**Solutions:**
- Restart Redis if unhealthy
- Clear cache and let it rebuild: `redis-cli FLUSHDB`
- Verify REDIS_URL environment variable
- Check Redis memory limits: `redis-cli INFO memory`
### Plan Rejection Not Auto-Regenerating
**Symptoms:** Rejected plans not triggering regeneration
**Checks:**
1. Check rejection notes contain auto-regenerate keywords
2. Verify RabbitMQ events are being published: Check `procurement.plan.rejected` queue
3. Check scheduler is listening to regeneration events
**Solutions:**
- Use keywords like "stale" or "outdated" in rejection notes
- Manually trigger regeneration via API
- Check RabbitMQ connectivity
- Verify event routing keys are correct
### Tenant Processing Timeouts
**Symptoms:** `tenant_processing_timeout_total` increasing
**Checks:**
1. Check timeout duration (180s for production, 120s for procurement)
2. Review slow queries in database logs
3. Check external service response times (Forecasting, Inventory)
4. Monitor CPU/memory usage during scheduler runs
**Solutions:**
- Increase timeout if consistently hitting limit
- Optimize database queries (add indexes)
- Scale external services if response time high
- Process fewer tenants in parallel (reduce concurrency)
---
## Maintenance
### Scheduled Maintenance Windows
When performing maintenance on schedulers:
1. **Announce downtime** to users (UI banner)
2. **Disable schedulers** temporarily:
```python
# Set environment variable
SCHEDULER_DISABLED=true
```
3. **Perform maintenance** (database migrations, service updates)
4. **Re-enable schedulers**:
```python
SCHEDULER_DISABLED=false
```
5. **Manually trigger** missed runs if needed:
```bash
curl -X POST http://service:8000/test/production-scheduler
curl -X POST http://service:8000/test/procurement-scheduler
```
### Database Migrations
When adding fields to scheduler-related tables:
1. **Create migration** with proper rollback
2. **Test migration** on staging environment
3. **Run migration** during low-traffic period (3-4 AM)
4. **Verify scheduler** still works after migration
5. **Monitor metrics** for anomalies
### Cache Maintenance
**Clear Stale Cache Entries:**
```bash
# Clear all forecast cache (will rebuild automatically)
redis-cli KEYS "forecast:*" | xargs redis-cli DEL
# Clear specific tenant's cache
redis-cli KEYS "forecast:{tenant_id}:*" | xargs redis-cli DEL
```
**Monitor Cache Size:**
```bash
# Check number of forecast keys
redis-cli DBSIZE
# Check memory usage
redis-cli INFO memory
```
---
## API Reference
### Production Scheduler Endpoints
```
POST /test/production-scheduler
Description: Manually trigger production scheduler (test mode)
Auth: Bearer token required
Response: {"message": "Production scheduler test triggered successfully"}
```
### Procurement Scheduler Endpoints
```
POST /test/procurement-scheduler
Description: Manually trigger procurement scheduler (test mode)
Auth: Bearer token required
Response: {"message": "Procurement scheduler test triggered successfully"}
```
### Forecast Cache Endpoints
```
GET /api/v1/{tenant_id}/forecasting/cache/stats
Description: Get forecast cache statistics
Auth: Bearer token required
Response: {
"available": true,
"total_forecast_keys": 1234,
"batch_forecast_keys": 45,
"single_forecast_keys": 1189,
"hit_rate_percent": 87.5,
...
}
DELETE /api/v1/{tenant_id}/forecasting/cache/product/{product_id}
Description: Invalidate forecast cache for specific product
Auth: Bearer token required (admin only)
Response: {"invalidated_keys": 7}
DELETE /api/v1/{tenant_id}/forecasting/cache
Description: Invalidate all forecast cache for tenant
Auth: Bearer token required (admin only)
Response: {"invalidated_keys": 123}
```
---
## Change Log
### Version 2.0 (2025-10-09) - Automated Scheduling
**Added:**
- ✨ ProductionSchedulerService for automated daily production planning
- ✨ Timezone configuration in Tenant model
- ✨ Forecast caching in Forecasting Service (service-level)
- ✨ Plan rejection workflow with auto-regeneration
- ✨ Comprehensive Prometheus metrics for monitoring
- ✨ TimezoneHelper utility for consistent timezone handling
**Changed:**
- 🔄 All schedulers now timezone-aware
- 🔄 Forecast service returns `cached: true` flag in metadata
- 🔄 Plan rejection triggers notifications and events
**Fixed:**
- 🐛 Duplicate forecast computations eliminated (50% reduction)
- 🐛 Timezone-related scheduling issues resolved
- 🐛 Rejected plans now have proper workflow handling
**Documentation:**
- 📚 Comprehensive production planning system documentation
- 📚 Runbooks for troubleshooting common issues
- 📚 Monitoring and alerting guidelines
### Version 1.0 (2025-10-07) - Initial Release
**Added:**
- ✨ ProcurementSchedulerService for automated procurement planning
- ✨ Daily, weekly, and cleanup jobs
- ✨ Leader election for distributed deployments
- ✨ Parallel tenant processing with timeouts
---
## Support & Contact
For issues or questions about the Production Planning System:
- **Documentation:** This file
- **Source Code:** `services/production/`, `services/orders/`
- **Issues:** GitHub Issues
- **Slack:** `#production-planning` channel
---
**Document Version:** 2.0
**Last Review Date:** 2025-10-09
**Next Review Date:** 2025-11-09