forked from gsn/predictor
feat: remove redis
This commit is contained in:
parent
7a9f81e527
commit
a850615e1f
18 changed files with 170 additions and 1142 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -56,3 +56,7 @@ Thumbs.db
|
||||||
# Leaflet WebUI
|
# Leaflet WebUI
|
||||||
/leaflet_predictor
|
/leaflet_predictor
|
||||||
/leaflet_predictor/*
|
/leaflet_predictor/*
|
||||||
|
|
||||||
|
# Tawhiri
|
||||||
|
/tawhiri
|
||||||
|
/tawhiri/*
|
||||||
501
DEPLOYMENT.md
501
DEPLOYMENT.md
|
|
@ -1,501 +0,0 @@
|
||||||
# Deployment Guide
|
|
||||||
|
|
||||||
This guide covers deploying the Predictor Service using Docker and Docker Compose.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- Docker Engine 20.10+
|
|
||||||
- Docker Compose 2.0+
|
|
||||||
- At least 2GB RAM available
|
|
||||||
- 10GB free disk space
|
|
||||||
|
|
||||||
## Quick Deployment
|
|
||||||
|
|
||||||
### 1. Clone and Setup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
cd predictor
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Validate Configuration
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Validate Docker configuration
|
|
||||||
./scripts/validate-docker.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Deploy
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build and start services
|
|
||||||
make up-build
|
|
||||||
|
|
||||||
# Check status
|
|
||||||
make ps
|
|
||||||
|
|
||||||
# View logs
|
|
||||||
make logs
|
|
||||||
```
|
|
||||||
|
|
||||||
## Production Deployment
|
|
||||||
|
|
||||||
### Environment Configuration
|
|
||||||
|
|
||||||
1. **Copy environment template:**
|
|
||||||
```bash
|
|
||||||
cp cmd/api/.env cmd/api/.env.production
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Edit production environment:**
|
|
||||||
```bash
|
|
||||||
nano cmd/api/.env.production
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Key production settings:**
|
|
||||||
```bash
|
|
||||||
# Security
|
|
||||||
GSN_PREDICTOR_REDIS_PASSWORD=your_secure_password
|
|
||||||
|
|
||||||
# Performance
|
|
||||||
GSN_PREDICTOR_GRIB_PARALLEL=8
|
|
||||||
GSN_PREDICTOR_GRIB_CACHE_TTL=2h
|
|
||||||
|
|
||||||
# Monitoring
|
|
||||||
GSN_PREDICTOR_GRIB_UPDATER_INTERVAL=3h
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production Docker Compose
|
|
||||||
|
|
||||||
Create `docker-compose.prod.yml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
predictor:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: predictor-prod
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
env_file:
|
|
||||||
- cmd/api/.env.production
|
|
||||||
volumes:
|
|
||||||
- grib_data:/tmp/grib
|
|
||||||
depends_on:
|
|
||||||
redis:
|
|
||||||
condition: service_healthy
|
|
||||||
networks:
|
|
||||||
- predictor-network
|
|
||||||
restart: unless-stopped
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 1G
|
|
||||||
cpus: '0.5'
|
|
||||||
reservations:
|
|
||||||
memory: 512M
|
|
||||||
cpus: '0.25'
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7.2-alpine
|
|
||||||
container_name: predictor-redis-prod
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
networks:
|
|
||||||
- predictor-network
|
|
||||||
restart: unless-stopped
|
|
||||||
command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru --requirepass ${GSN_PREDICTOR_REDIS_PASSWORD}
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "-a", "${GSN_PREDICTOR_REDIS_PASSWORD}", "ping"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 5
|
|
||||||
start_period: 10s
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
grib_data:
|
|
||||||
driver: local
|
|
||||||
redis_data:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
networks:
|
|
||||||
predictor-network:
|
|
||||||
driver: bridge
|
|
||||||
```
|
|
||||||
|
|
||||||
### Deploy to Production
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Deploy with production config
|
|
||||||
docker-compose -f docker-compose.prod.yml up -d
|
|
||||||
|
|
||||||
# Monitor deployment
|
|
||||||
docker-compose -f docker-compose.prod.yml logs -f
|
|
||||||
|
|
||||||
# Check health
|
|
||||||
curl http://localhost:8080/health
|
|
||||||
```
|
|
||||||
|
|
||||||
## Kubernetes Deployment
|
|
||||||
|
|
||||||
### Create Namespace
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# k8s/namespace.yaml
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: predictor
|
|
||||||
```
|
|
||||||
|
|
||||||
### Redis Deployment
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# k8s/redis.yaml
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: redis
|
|
||||||
namespace: predictor
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: redis
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: redis
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: redis
|
|
||||||
image: redis:7.2-alpine
|
|
||||||
ports:
|
|
||||||
- containerPort: 6379
|
|
||||||
command: ["redis-server", "--appendonly", "yes", "--maxmemory", "512mb", "--maxmemory-policy", "allkeys-lru"]
|
|
||||||
volumeMounts:
|
|
||||||
- name: redis-data
|
|
||||||
mountPath: /data
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: "512Mi"
|
|
||||||
cpu: "250m"
|
|
||||||
requests:
|
|
||||||
memory: "256Mi"
|
|
||||||
cpu: "100m"
|
|
||||||
livenessProbe:
|
|
||||||
exec:
|
|
||||||
command: ["redis-cli", "ping"]
|
|
||||||
initialDelaySeconds: 10
|
|
||||||
periodSeconds: 10
|
|
||||||
readinessProbe:
|
|
||||||
exec:
|
|
||||||
command: ["redis-cli", "ping"]
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 5
|
|
||||||
volumes:
|
|
||||||
- name: redis-data
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: redis-pvc
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: redis
|
|
||||||
namespace: predictor
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app: redis
|
|
||||||
ports:
|
|
||||||
- port: 6379
|
|
||||||
targetPort: 6379
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
name: redis-pvc
|
|
||||||
namespace: predictor
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 10Gi
|
|
||||||
```
|
|
||||||
|
|
||||||
### Predictor Deployment
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# k8s/predictor.yaml
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: predictor
|
|
||||||
namespace: predictor
|
|
||||||
spec:
|
|
||||||
replicas: 2
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: predictor
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: predictor
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: predictor
|
|
||||||
image: predictor:latest
|
|
||||||
ports:
|
|
||||||
- containerPort: 8080
|
|
||||||
env:
|
|
||||||
- name: GSN_PREDICTOR_REDIS_HOST
|
|
||||||
value: "redis"
|
|
||||||
- name: GSN_PREDICTOR_REDIS_PORT
|
|
||||||
value: "6379"
|
|
||||||
- name: GSN_PREDICTOR_GRIB_DIR
|
|
||||||
value: "/tmp/grib"
|
|
||||||
- name: GSN_PREDICTOR_SCHEDULER_ENABLED
|
|
||||||
value: "true"
|
|
||||||
- name: GSN_PREDICTOR_GRIB_UPDATER_INTERVAL
|
|
||||||
value: "6h"
|
|
||||||
- name: GSN_PREDICTOR_GRIB_UPDATER_TIMEOUT
|
|
||||||
value: "45m"
|
|
||||||
volumeMounts:
|
|
||||||
- name: grib-data
|
|
||||||
mountPath: /tmp/grib
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: "1Gi"
|
|
||||||
cpu: "500m"
|
|
||||||
requests:
|
|
||||||
memory: "512Mi"
|
|
||||||
cpu: "250m"
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /health
|
|
||||||
port: 8080
|
|
||||||
initialDelaySeconds: 40
|
|
||||||
periodSeconds: 30
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /health
|
|
||||||
port: 8080
|
|
||||||
initialDelaySeconds: 10
|
|
||||||
periodSeconds: 10
|
|
||||||
volumes:
|
|
||||||
- name: grib-data
|
|
||||||
emptyDir: {}
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: predictor
|
|
||||||
namespace: predictor
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app: predictor
|
|
||||||
ports:
|
|
||||||
- port: 80
|
|
||||||
targetPort: 8080
|
|
||||||
type: LoadBalancer
|
|
||||||
```
|
|
||||||
|
|
||||||
### Deploy to Kubernetes
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Apply namespace
|
|
||||||
kubectl apply -f k8s/namespace.yaml
|
|
||||||
|
|
||||||
# Apply Redis
|
|
||||||
kubectl apply -f k8s/redis.yaml
|
|
||||||
|
|
||||||
# Wait for Redis to be ready
|
|
||||||
kubectl wait --for=condition=ready pod -l app=redis -n predictor
|
|
||||||
|
|
||||||
# Apply Predictor
|
|
||||||
kubectl apply -f k8s/predictor.yaml
|
|
||||||
|
|
||||||
# Check status
|
|
||||||
kubectl get pods -n predictor
|
|
||||||
kubectl get services -n predictor
|
|
||||||
```
|
|
||||||
|
|
||||||
## Monitoring and Logging
|
|
||||||
|
|
||||||
### Health Checks
|
|
||||||
|
|
||||||
The service includes built-in health checks:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Application health
|
|
||||||
curl http://localhost:8080/health
|
|
||||||
|
|
||||||
# Docker health
|
|
||||||
docker inspect predictor | jq '.[0].State.Health'
|
|
||||||
|
|
||||||
# Kubernetes health
|
|
||||||
kubectl describe pod -l app=predictor -n predictor
|
|
||||||
```
|
|
||||||
|
|
||||||
### Logging
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Docker logs
|
|
||||||
docker-compose logs -f predictor
|
|
||||||
|
|
||||||
# Kubernetes logs
|
|
||||||
kubectl logs -f deployment/predictor -n predictor
|
|
||||||
```
|
|
||||||
|
|
||||||
### Metrics
|
|
||||||
|
|
||||||
Consider adding Prometheus metrics:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# Add to docker-compose.yml
|
|
||||||
prometheus:
|
|
||||||
image: prom/prometheus:latest
|
|
||||||
ports:
|
|
||||||
- "9090:9090"
|
|
||||||
volumes:
|
|
||||||
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
|
|
||||||
networks:
|
|
||||||
- predictor-network
|
|
||||||
```
|
|
||||||
|
|
||||||
## Backup and Recovery
|
|
||||||
|
|
||||||
### Redis Backup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create backup
|
|
||||||
docker exec predictor-redis redis-cli BGSAVE
|
|
||||||
|
|
||||||
# Copy backup file
|
|
||||||
docker cp predictor-redis:/data/dump.rdb ./backup/redis-$(date +%Y%m%d).rdb
|
|
||||||
```
|
|
||||||
|
|
||||||
### GRIB Data Backup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Backup GRIB data
|
|
||||||
docker run --rm -v predictor_grib_data:/data -v $(pwd)/backup:/backup alpine tar czf /backup/grib-$(date +%Y%m%d).tar.gz -C /data .
|
|
||||||
```
|
|
||||||
|
|
||||||
### Automated Backup Script
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
# scripts/backup.sh
|
|
||||||
|
|
||||||
BACKUP_DIR="./backup/$(date +%Y%m%d)"
|
|
||||||
mkdir -p $BACKUP_DIR
|
|
||||||
|
|
||||||
# Redis backup
|
|
||||||
docker exec predictor-redis redis-cli BGSAVE
|
|
||||||
sleep 5
|
|
||||||
docker cp predictor-redis:/data/dump.rdb $BACKUP_DIR/redis.rdb
|
|
||||||
|
|
||||||
# GRIB data backup
|
|
||||||
docker run --rm -v predictor_grib_data:/data -v $(pwd)/$BACKUP_DIR:/backup alpine tar czf /backup/grib.tar.gz -C /data .
|
|
||||||
|
|
||||||
echo "Backup completed: $BACKUP_DIR"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
1. **Redis Connection Issues:**
|
|
||||||
```bash
|
|
||||||
# Check Redis status
|
|
||||||
docker-compose exec redis redis-cli ping
|
|
||||||
|
|
||||||
# Check network connectivity
|
|
||||||
docker-compose exec predictor wget -O- http://redis:6379
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **GRIB Download Failures:**
|
|
||||||
```bash
|
|
||||||
# Check disk space
|
|
||||||
docker-compose exec predictor df -h /tmp/grib
|
|
||||||
|
|
||||||
# Check internet connectivity
|
|
||||||
docker-compose exec predictor wget -O- https://nomads.ncep.noaa.gov/
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Memory Issues:**
|
|
||||||
```bash
|
|
||||||
# Check memory usage
|
|
||||||
docker stats
|
|
||||||
|
|
||||||
# Check container logs
|
|
||||||
docker-compose logs predictor | grep -i memory
|
|
||||||
```
|
|
||||||
|
|
||||||
### Performance Tuning
|
|
||||||
|
|
||||||
1. **Redis Optimization:**
|
|
||||||
```bash
|
|
||||||
# Increase Redis memory
|
|
||||||
GSN_PREDICTOR_REDIS_MAXMEMORY=1gb
|
|
||||||
|
|
||||||
# Optimize Redis settings
|
|
||||||
redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **GRIB Processing:**
|
|
||||||
```bash
|
|
||||||
# Increase parallel workers
|
|
||||||
GSN_PREDICTOR_GRIB_PARALLEL=8
|
|
||||||
|
|
||||||
# Optimize cache TTL
|
|
||||||
GSN_PREDICTOR_GRIB_CACHE_TTL=2h
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Container Resources:**
|
|
||||||
```yaml
|
|
||||||
# In docker-compose.yml
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 2G
|
|
||||||
cpus: '1.0'
|
|
||||||
reservations:
|
|
||||||
memory: 1G
|
|
||||||
cpus: '0.5'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
1. **Network Security:**
|
|
||||||
- Use internal networks for service communication
|
|
||||||
- Expose only necessary ports
|
|
||||||
- Use reverse proxy for external access
|
|
||||||
|
|
||||||
2. **Container Security:**
|
|
||||||
- Run as non-root user
|
|
||||||
- Use minimal base images
|
|
||||||
- Regular security updates
|
|
||||||
|
|
||||||
3. **Data Security:**
|
|
||||||
- Encrypt sensitive environment variables
|
|
||||||
- Use secrets management for passwords
|
|
||||||
- Regular backups
|
|
||||||
|
|
||||||
4. **Access Control:**
|
|
||||||
- Implement API authentication
|
|
||||||
- Use HTTPS in production
|
|
||||||
- Monitor access logs
|
|
||||||
```
|
|
||||||
15
Makefile
15
Makefile
|
|
@ -43,10 +43,6 @@ logs:
|
||||||
logs-predictor:
|
logs-predictor:
|
||||||
docker-compose -f $(COMPOSE_FILE) logs -f predictor
|
docker-compose -f $(COMPOSE_FILE) logs -f predictor
|
||||||
|
|
||||||
# View logs for Redis
|
|
||||||
.PHONY: logs-redis
|
|
||||||
logs-redis:
|
|
||||||
docker-compose -f $(COMPOSE_FILE) logs -f redis
|
|
||||||
|
|
||||||
# Check service status
|
# Check service status
|
||||||
.PHONY: ps
|
.PHONY: ps
|
||||||
|
|
@ -58,11 +54,6 @@ ps:
|
||||||
exec:
|
exec:
|
||||||
docker-compose -f $(COMPOSE_FILE) exec predictor sh
|
docker-compose -f $(COMPOSE_FILE) exec predictor sh
|
||||||
|
|
||||||
# Execute command in Redis container
|
|
||||||
.PHONY: exec-redis
|
|
||||||
exec-redis:
|
|
||||||
docker-compose -f $(COMPOSE_FILE) exec redis sh
|
|
||||||
|
|
||||||
# Clean up Docker resources
|
# Clean up Docker resources
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
|
|
@ -79,7 +70,7 @@ test:
|
||||||
build-local:
|
build-local:
|
||||||
go build -o predictor ./cmd/api
|
go build -o predictor ./cmd/api
|
||||||
|
|
||||||
# Run locally (requires Redis)
|
# Run locally
|
||||||
.PHONY: run-local
|
.PHONY: run-local
|
||||||
run-local:
|
run-local:
|
||||||
cd cmd/api && go run .
|
cd cmd/api && go run .
|
||||||
|
|
@ -106,14 +97,12 @@ help:
|
||||||
@echo " down-volumes - Stop services and remove volumes"
|
@echo " down-volumes - Stop services and remove volumes"
|
||||||
@echo " logs - View all logs"
|
@echo " logs - View all logs"
|
||||||
@echo " logs-predictor - View predictor logs"
|
@echo " logs-predictor - View predictor logs"
|
||||||
@echo " logs-redis - View Redis logs"
|
|
||||||
@echo " ps - Show service status"
|
@echo " ps - Show service status"
|
||||||
@echo " exec - Execute shell in predictor container"
|
@echo " exec - Execute shell in predictor container"
|
||||||
@echo " exec-redis - Execute shell in Redis container"
|
|
||||||
@echo " clean - Clean up Docker resources"
|
@echo " clean - Clean up Docker resources"
|
||||||
@echo " test - Run tests"
|
@echo " test - Run tests"
|
||||||
@echo " build-local - Build locally"
|
@echo " build-local - Build locally"
|
||||||
@echo " run-local - Run locally (requires Redis)"
|
@echo " run-local - Run locally"
|
||||||
@echo " fmt - Format code"
|
@echo " fmt - Format code"
|
||||||
@echo " lint - Lint code"
|
@echo " lint - Lint code"
|
||||||
@echo " help - Show this help"
|
@echo " help - Show this help"
|
||||||
|
|
|
||||||
261
README.md
261
README.md
|
|
@ -1,261 +0,0 @@
|
||||||
# Predictor Service
|
|
||||||
|
|
||||||
A Go-based weather prediction service that downloads and processes GRIB files for wind vector data extraction.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **GRIB File Processing**: Downloads and processes GRIB weather data files
|
|
||||||
- **Wind Vector Extraction**: Extracts wind vector data for given coordinates and time
|
|
||||||
- **Redis Caching**: Caches extraction results for improved performance
|
|
||||||
- **Distributed Locking**: Uses Redis-based distributed locks for safe concurrent downloads
|
|
||||||
- **Scheduled Updates**: Automatic GRIB file updates via configurable scheduler
|
|
||||||
- **REST API**: HTTP API for data extraction and service management
|
|
||||||
- **Modular Architecture**: Clean separation of concerns with dependency injection
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
The service follows a modular architecture with clear separation of concerns:
|
|
||||||
|
|
||||||
- **Service Layer**: Business logic and orchestration
|
|
||||||
- **GRIB Package**: GRIB file processing and data extraction
|
|
||||||
- **Redis Package**: Caching and distributed locking
|
|
||||||
- **Scheduler Package**: Job scheduling and execution
|
|
||||||
- **Transport Layer**: HTTP API handling
|
|
||||||
- **Jobs**: Background tasks (GRIB updates, etc.)
|
|
||||||
|
|
||||||
## Quick Start with Docker
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- Docker
|
|
||||||
- Docker Compose
|
|
||||||
|
|
||||||
### Running the Service
|
|
||||||
|
|
||||||
1. **Clone the repository:**
|
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
cd predictor
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Start the services:**
|
|
||||||
```bash
|
|
||||||
# Production
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Development (with volume mounts)
|
|
||||||
docker-compose -f docker-compose.dev.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Check service status:**
|
|
||||||
```bash
|
|
||||||
docker-compose ps
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **View logs:**
|
|
||||||
```bash
|
|
||||||
# All services
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# Specific service
|
|
||||||
docker-compose logs -f predictor
|
|
||||||
docker-compose logs -f redis
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using Make Commands
|
|
||||||
|
|
||||||
The project includes a Makefile for common operations:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build and start services
|
|
||||||
make up-build
|
|
||||||
|
|
||||||
# View logs
|
|
||||||
make logs
|
|
||||||
|
|
||||||
# Stop services
|
|
||||||
make down
|
|
||||||
|
|
||||||
# Clean up everything
|
|
||||||
make clean
|
|
||||||
|
|
||||||
# Show all available commands
|
|
||||||
make help
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
The service is configured via environment variables with the prefix `GSN_PREDICTOR_`:
|
|
||||||
|
|
||||||
### GRIB Configuration
|
|
||||||
- `GSN_PREDICTOR_GRIB_DIR`: Directory for GRIB files (default: `/tmp/grib`)
|
|
||||||
- `GSN_PREDICTOR_GRIB_TTL`: GRIB file TTL (default: `24h`)
|
|
||||||
- `GSN_PREDICTOR_GRIB_CACHE_TTL`: Cache TTL (default: `1h`)
|
|
||||||
- `GSN_PREDICTOR_GRIB_PARALLEL`: Parallel download workers (default: `4`)
|
|
||||||
- `GSN_PREDICTOR_GRIB_TIMEOUT`: Download timeout (default: `30s`)
|
|
||||||
- `GSN_PREDICTOR_GRIB_DATASET_URL`: GRIB data source URL
|
|
||||||
|
|
||||||
### Redis Configuration
|
|
||||||
- `GSN_PREDICTOR_REDIS_HOST`: Redis host (default: `localhost`)
|
|
||||||
- `GSN_PREDICTOR_REDIS_PORT`: Redis port (default: `6379`)
|
|
||||||
- `GSN_PREDICTOR_REDIS_PASSWORD`: Redis password (default: empty)
|
|
||||||
- `GSN_PREDICTOR_REDIS_DB`: Redis database (default: `0`)
|
|
||||||
|
|
||||||
### Scheduler Configuration
|
|
||||||
- `GSN_PREDICTOR_SCHEDULER_ENABLED`: Enable scheduler (default: `true`)
|
|
||||||
|
|
||||||
### GRIB Updater Job Configuration
|
|
||||||
- `GSN_PREDICTOR_GRIB_UPDATER_INTERVAL`: Update interval (default: `6h`)
|
|
||||||
- `GSN_PREDICTOR_GRIB_UPDATER_TIMEOUT`: Update timeout (default: `45m`)
|
|
||||||
|
|
||||||
### REST Transport Configuration
|
|
||||||
- `GSN_PREDICTOR_REST_HOST`: HTTP host (default: `0.0.0.0`)
|
|
||||||
- `GSN_PREDICTOR_REST_PORT`: HTTP port (default: `8080`)
|
|
||||||
- `GSN_PREDICTOR_REST_READ_TIMEOUT`: Read timeout (default: `30s`)
|
|
||||||
- `GSN_PREDICTOR_REST_WRITE_TIMEOUT`: Write timeout (default: `30s`)
|
|
||||||
- `GSN_PREDICTOR_REST_IDLE_TIMEOUT`: Idle timeout (default: `60s`)
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
The service exposes a REST API for wind data extraction:
|
|
||||||
|
|
||||||
- `GET /health` - Health check endpoint
|
|
||||||
- `POST /predict` - Extract wind data for given coordinates and time
|
|
||||||
|
|
||||||
### Example API Usage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Health check
|
|
||||||
curl http://localhost:8080/health
|
|
||||||
|
|
||||||
# Extract wind data
|
|
||||||
curl -X POST http://localhost:8080/predict \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"latitude": 40.7128,
|
|
||||||
"longitude": -74.0060,
|
|
||||||
"altitude": 100,
|
|
||||||
"timestamp": "2024-01-15T12:00:00Z"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
### Local Development
|
|
||||||
|
|
||||||
1. **Install dependencies:**
|
|
||||||
```bash
|
|
||||||
go mod download
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Start Redis:**
|
|
||||||
```bash
|
|
||||||
docker run -d --name redis -p 6379:6379 redis:7.2-alpine
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Set environment variables:**
|
|
||||||
```bash
|
|
||||||
cd cmd/api
|
|
||||||
source .env
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Run the service:**
|
|
||||||
```bash
|
|
||||||
go run .
|
|
||||||
```
|
|
||||||
|
|
||||||
### Building Locally
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build the binary
|
|
||||||
make build-local
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
make test
|
|
||||||
|
|
||||||
# Format code
|
|
||||||
make fmt
|
|
||||||
|
|
||||||
# Lint code
|
|
||||||
make lint
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Development
|
|
||||||
|
|
||||||
For development with hot reloading:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start development environment
|
|
||||||
docker-compose -f docker-compose.dev.yml up -d
|
|
||||||
|
|
||||||
# View logs
|
|
||||||
docker-compose -f docker-compose.dev.yml logs -f predictor
|
|
||||||
```
|
|
||||||
|
|
||||||
## Docker Best Practices
|
|
||||||
|
|
||||||
The Dockerfile follows Go best practices:
|
|
||||||
|
|
||||||
- **Multi-stage build**: Separate builder and runtime stages
|
|
||||||
- **Non-root user**: Runs as non-root user for security
|
|
||||||
- **Minimal runtime**: Uses Alpine Linux for smaller image size
|
|
||||||
- **Health checks**: Built-in health monitoring
|
|
||||||
- **Optimized layers**: Efficient layer caching
|
|
||||||
- **Security**: No unnecessary packages or permissions
|
|
||||||
|
|
||||||
## Monitoring and Health Checks
|
|
||||||
|
|
||||||
The service includes built-in health checks:
|
|
||||||
|
|
||||||
- **Application health**: HTTP endpoint at `/health`
|
|
||||||
- **Docker health**: Container health check with wget
|
|
||||||
- **Redis health**: Redis ping health check
|
|
||||||
- **Service dependencies**: Proper startup order with health checks
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
1. **Redis connection refused:**
|
|
||||||
- Ensure Redis is running: `docker-compose ps`
|
|
||||||
- Check Redis logs: `docker-compose logs redis`
|
|
||||||
- Verify network connectivity
|
|
||||||
|
|
||||||
2. **GRIB download failures:**
|
|
||||||
- Check internet connectivity
|
|
||||||
- Verify GRIB data source URL
|
|
||||||
- Check disk space in `/tmp/grib`
|
|
||||||
|
|
||||||
3. **Service not starting:**
|
|
||||||
- Check logs: `docker-compose logs predictor`
|
|
||||||
- Verify environment variables
|
|
||||||
- Check port conflicts
|
|
||||||
|
|
||||||
### Debug Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Execute shell in container
|
|
||||||
docker-compose exec predictor sh
|
|
||||||
|
|
||||||
# Check Redis connectivity
|
|
||||||
docker-compose exec redis redis-cli ping
|
|
||||||
|
|
||||||
# View container resources
|
|
||||||
docker stats
|
|
||||||
|
|
||||||
# Check network connectivity
|
|
||||||
docker-compose exec predictor wget -O- http://redis:6379
|
|
||||||
```
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
1. Fork the repository
|
|
||||||
2. Create a feature branch
|
|
||||||
3. Make your changes
|
|
||||||
4. Add tests
|
|
||||||
5. Run tests and linting
|
|
||||||
6. Submit a pull request
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[Add your license information here]
|
|
||||||
|
|
@ -14,7 +14,6 @@ import (
|
||||||
"git.intra.yksa.space/gsn/predictor/internal/service"
|
"git.intra.yksa.space/gsn/predictor/internal/service"
|
||||||
"git.intra.yksa.space/gsn/predictor/internal/transport/rest"
|
"git.intra.yksa.space/gsn/predictor/internal/transport/rest"
|
||||||
"git.intra.yksa.space/gsn/predictor/internal/transport/rest/handler"
|
"git.intra.yksa.space/gsn/predictor/internal/transport/rest/handler"
|
||||||
"git.intra.yksa.space/gsn/predictor/pkg/redis"
|
|
||||||
"git.intra.yksa.space/gsn/predictor/pkg/scheduler"
|
"git.intra.yksa.space/gsn/predictor/pkg/scheduler"
|
||||||
env "github.com/caarlos0/env/v11"
|
env "github.com/caarlos0/env/v11"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
@ -45,23 +44,10 @@ func main() {
|
||||||
log.Ctx(ctx).Fatal("failed to load GRIB updater configuration", zap.Error(err))
|
log.Ctx(ctx).Fatal("failed to load GRIB updater configuration", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Ctx(ctx).Info("Connecting to Redis", zap.String("host", cfg.RedisHost), zap.Int("port", cfg.RedisPort))
|
|
||||||
redisService, err := redis.New(redis.Config{
|
|
||||||
Host: cfg.RedisHost,
|
|
||||||
Port: cfg.RedisPort,
|
|
||||||
Password: cfg.RedisPassword,
|
|
||||||
DB: cfg.RedisDB,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Ctx(ctx).Fatal("failed to initialize Redis service", zap.Error(err), zap.String("host", cfg.RedisHost), zap.Int("port", cfg.RedisPort))
|
|
||||||
}
|
|
||||||
defer redisService.Close()
|
|
||||||
|
|
||||||
gribService, err := grib.New(grib.ServiceConfig{
|
gribService, err := grib.New(grib.ServiceConfig{
|
||||||
Dir: cfg.GribDir,
|
Dir: cfg.GribDir,
|
||||||
TTL: cfg.GribTTL,
|
TTL: cfg.GribTTL,
|
||||||
CacheTTL: cfg.GribCacheTTL,
|
CacheTTL: cfg.GribCacheTTL,
|
||||||
Redis: redisService,
|
|
||||||
Parallel: cfg.GribParallel,
|
Parallel: cfg.GribParallel,
|
||||||
Client: cfg.CreateHTTPClient(),
|
Client: cfg.CreateHTTPClient(),
|
||||||
DatasetURL: cfg.GribDatasetURL,
|
DatasetURL: cfg.GribDatasetURL,
|
||||||
|
|
@ -81,7 +67,7 @@ func main() {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
svc, err := service.New(cfg, gribService, redisService)
|
svc, err := service.New(cfg, gribService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Ctx(ctx).Fatal("failed to initialize service", zap.Error(err))
|
log.Ctx(ctx).Fatal("failed to initialize service", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
predictor:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: predictor
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
environment:
|
|
||||||
# --- GRIB Configuration ---
|
|
||||||
- GSN_PREDICTOR_GRIB_DIR=/tmp/grib
|
|
||||||
- GSN_PREDICTOR_GRIB_TTL=24h
|
|
||||||
- GSN_PREDICTOR_GRIB_CACHE_TTL=1h
|
|
||||||
- GSN_PREDICTOR_GRIB_PARALLEL=4
|
|
||||||
- GSN_PREDICTOR_GRIB_TIMEOUT=30s
|
|
||||||
- GSN_PREDICTOR_GRIB_DATASET_URL=https://nomads.ncep.noaa.gov/pub/data/nccf/com/gfs/prod
|
|
||||||
|
|
||||||
# --- Redis Configuration ---
|
|
||||||
- GSN_PREDICTOR_REDIS_HOST=redis
|
|
||||||
- GSN_PREDICTOR_REDIS_PORT=6379
|
|
||||||
- GSN_PREDICTOR_REDIS_PASSWORD=
|
|
||||||
- GSN_PREDICTOR_REDIS_DB=0
|
|
||||||
|
|
||||||
# --- Scheduler Configuration ---
|
|
||||||
- GSN_PREDICTOR_SCHEDULER_ENABLED=true
|
|
||||||
|
|
||||||
# --- GRIB Updater Job Configuration ---
|
|
||||||
- GSN_PREDICTOR_GRIB_UPDATER_INTERVAL=6h
|
|
||||||
- GSN_PREDICTOR_GRIB_UPDATER_TIMEOUT=45m
|
|
||||||
|
|
||||||
# --- REST Transport Configuration ---
|
|
||||||
- GSN_PREDICTOR_REST_HOST=0.0.0.0
|
|
||||||
- GSN_PREDICTOR_REST_PORT=8080
|
|
||||||
- GSN_PREDICTOR_REST_READ_TIMEOUT=30s
|
|
||||||
- GSN_PREDICTOR_REST_WRITE_TIMEOUT=30s
|
|
||||||
- GSN_PREDICTOR_REST_IDLE_TIMEOUT=60s
|
|
||||||
volumes:
|
|
||||||
- ./grib_data:/tmp/grib
|
|
||||||
depends_on:
|
|
||||||
redis:
|
|
||||||
condition: service_healthy
|
|
||||||
networks:
|
|
||||||
- predictor-network
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/ready"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7.2-alpine
|
|
||||||
container_name: predictor-redis
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
networks:
|
|
||||||
- predictor-network
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 5
|
|
||||||
start_period: 10s
|
|
||||||
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
redis_data:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
networks:
|
|
||||||
predictor-network:
|
|
||||||
driver: bridge
|
|
||||||
37
go.mod
37
go.mod
|
|
@ -5,41 +5,38 @@ go 1.24.4
|
||||||
require (
|
require (
|
||||||
github.com/caarlos0/env/v11 v11.3.1
|
github.com/caarlos0/env/v11 v11.3.1
|
||||||
github.com/edsrzf/mmap-go v1.2.0
|
github.com/edsrzf/mmap-go v1.2.0
|
||||||
|
github.com/go-co-op/gocron v1.37.0
|
||||||
github.com/go-faster/errors v0.7.1
|
github.com/go-faster/errors v0.7.1
|
||||||
github.com/go-faster/jx v1.1.0
|
github.com/go-faster/jx v1.1.0
|
||||||
github.com/nilsmagnus/grib v1.2.8
|
github.com/nilsmagnus/grib v1.2.8
|
||||||
github.com/ogen-go/ogen v1.14.0
|
github.com/ogen-go/ogen v1.16.0
|
||||||
go.opentelemetry.io/otel v1.36.0
|
github.com/rs/cors v1.11.1
|
||||||
go.opentelemetry.io/otel/metric v1.36.0
|
go.opentelemetry.io/otel v1.38.0
|
||||||
go.opentelemetry.io/otel/trace v1.36.0
|
go.opentelemetry.io/otel/metric v1.38.0
|
||||||
|
go.opentelemetry.io/otel/trace v1.38.0
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
golang.org/x/sync v0.14.0
|
golang.org/x/sync v0.17.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bsm/redislock v0.9.4 // indirect
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
|
||||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/fatih/color v1.18.0 // indirect
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
github.com/ghodss/yaml v1.0.0 // indirect
|
github.com/ghodss/yaml v1.0.0 // indirect
|
||||||
github.com/go-co-op/gocron v1.37.0 // indirect
|
|
||||||
github.com/go-faster/yaml v0.4.6 // indirect
|
github.com/go-faster/yaml v0.4.6 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/redis/go-redis/v9 v9.10.0 // indirect
|
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
github.com/rs/cors v1.11.1 // indirect
|
github.com/segmentio/asm v1.2.1 // indirect
|
||||||
github.com/segmentio/asm v1.2.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.uber.org/atomic v1.9.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 // indirect
|
golang.org/x/exp v0.0.0-20251017212417-90e834f514db // indirect
|
||||||
golang.org/x/net v0.40.0 // indirect
|
golang.org/x/net v0.46.0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.37.0 // indirect
|
||||||
golang.org/x/text v0.25.0 // indirect
|
golang.org/x/text v0.30.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
40
go.sum
40
go.sum
|
|
@ -1,15 +1,9 @@
|
||||||
github.com/bsm/redislock v0.9.4 h1:X/Wse1DPpiQgHbVYRE9zv6m070UcKoOGekgvpNhiSvw=
|
|
||||||
github.com/bsm/redislock v0.9.4/go.mod h1:Epf7AJLiSFwLCiZcfi6pWFO/8eAYrYpQXFxEDPoDeAk=
|
|
||||||
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
||||||
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
|
||||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84=
|
github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84=
|
||||||
|
|
@ -29,6 +23,8 @@ github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbX
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
|
@ -47,6 +43,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
|
@ -54,21 +52,26 @@ github.com/nilsmagnus/grib v1.2.8 h1:H7ch/1/agaCqM3MC8hW1Ft+EJ+q2XB757uml/IfPvp4
|
||||||
github.com/nilsmagnus/grib v1.2.8/go.mod h1:XHm+5zuoOk0NSIWaGmA3JaAxI4i50YvD1L1vz+aqPOQ=
|
github.com/nilsmagnus/grib v1.2.8/go.mod h1:XHm+5zuoOk0NSIWaGmA3JaAxI4i50YvD1L1vz+aqPOQ=
|
||||||
github.com/ogen-go/ogen v1.14.0 h1:TU1Nj4z9UBsAfTkf+IhuNNp7igdFQKqkk9+6/y4XuWg=
|
github.com/ogen-go/ogen v1.14.0 h1:TU1Nj4z9UBsAfTkf+IhuNNp7igdFQKqkk9+6/y4XuWg=
|
||||||
github.com/ogen-go/ogen v1.14.0/go.mod h1:Iw1vkqkx6SU7I9th5ceP+fVPJ6Wge4e3kAVzAxJEpPE=
|
github.com/ogen-go/ogen v1.14.0/go.mod h1:Iw1vkqkx6SU7I9th5ceP+fVPJ6Wge4e3kAVzAxJEpPE=
|
||||||
|
github.com/ogen-go/ogen v1.16.0 h1:fKHEYokW/QrMzVNXId74/6RObRIUs9T2oroGKtR25Iw=
|
||||||
|
github.com/ogen-go/ogen v1.16.0/go.mod h1:s3nWiMzybSf8fhxckyO+wtto92+QHpEL8FmkPnhL3jI=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
|
|
||||||
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
||||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||||
|
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||||
|
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||||
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
|
@ -78,16 +81,27 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
||||||
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
||||||
|
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||||
|
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||||
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
||||||
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
||||||
|
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||||
|
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||||
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
||||||
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
||||||
|
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||||
|
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
|
@ -96,16 +110,26 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
|
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
|
||||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||||
|
golang.org/x/exp v0.0.0-20251017212417-90e834f514db h1:by6IehL4BH5k3e3SJmcoNbOobMey2SLpAF79iPOEBvw=
|
||||||
|
golang.org/x/exp v0.0.0-20251017212417-90e834f514db/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||||
|
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||||
|
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||||
|
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||||
|
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||||
|
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
|
|
||||||
|
|
@ -91,10 +91,6 @@ var (
|
||||||
ErrConfig = New(http.StatusInternalServerError, "configuration error")
|
ErrConfig = New(http.StatusInternalServerError, "configuration error")
|
||||||
ErrConfigInvalidEnv = New(http.StatusInternalServerError, "invalid environment configuration")
|
ErrConfigInvalidEnv = New(http.StatusInternalServerError, "invalid environment configuration")
|
||||||
ErrConfigMissingRequired = New(http.StatusInternalServerError, "missing required configuration")
|
ErrConfigMissingRequired = New(http.StatusInternalServerError, "missing required configuration")
|
||||||
ErrRedis = New(http.StatusInternalServerError, "redis error")
|
|
||||||
ErrRedisLockAlreadyLocked = New(http.StatusConflict, "could not perform redis lock", "already locked")
|
|
||||||
ErrRedisCacheMiss = New(http.StatusNotFound, "cache miss", "key not found")
|
|
||||||
ErrRedisCacheCorrupted = New(http.StatusInternalServerError, "cache data corrupted", "invalid format")
|
|
||||||
ErrDownload = New(http.StatusInternalServerError, "download error")
|
ErrDownload = New(http.StatusInternalServerError, "download error")
|
||||||
ErrProcessing = New(http.StatusInternalServerError, "data processing error")
|
ErrProcessing = New(http.StatusInternalServerError, "data processing error")
|
||||||
ErrNoCubeFilesFound = New(http.StatusNotFound, "no cube files found")
|
ErrNoCubeFilesFound = New(http.StatusNotFound, "no cube files found")
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
- **Сборка 5D-куба** (время, давление, широта, долгота, переменные u/v)
|
- **Сборка 5D-куба** (время, давление, широта, долгота, переменные u/v)
|
||||||
- **Эффективное хранение** с использованием mmap
|
- **Эффективное хранение** с использованием mmap
|
||||||
- **Интерполяция** ветровых данных для произвольных координат и времени
|
- **Интерполяция** ветровых данных для произвольных координат и времени
|
||||||
- **Кэширование** результатов (in-memory + Redis)
|
- **Кэширование** результатов (in-memory)
|
||||||
- **Распределенные блокировки** для предотвращения дублирования загрузок
|
- **Распределенные блокировки** для предотвращения дублирования загрузок
|
||||||
|
|
||||||
## Архитектура
|
## Архитектура
|
||||||
|
|
@ -38,7 +38,6 @@ cfg := grib.ServiceConfig{
|
||||||
Dir: "/tmp/grib",
|
Dir: "/tmp/grib",
|
||||||
TTL: 24 * time.Hour,
|
TTL: 24 * time.Hour,
|
||||||
CacheTTL: 1 * time.Hour,
|
CacheTTL: 1 * time.Hour,
|
||||||
Redis: redisClient,
|
|
||||||
Parallel: 4,
|
Parallel: 4,
|
||||||
Client: &http.Client{Timeout: 30 * time.Second},
|
Client: &http.Client{Timeout: 30 * time.Second},
|
||||||
}
|
}
|
||||||
|
|
@ -68,7 +67,6 @@ wind, err := service.Extract(ctx, lat, lon, alt, timestamp)
|
||||||
## Кэширование
|
## Кэширование
|
||||||
|
|
||||||
- **In-memory кэш**: быстрый доступ к недавно запрошенным данным
|
- **In-memory кэш**: быстрый доступ к недавно запрошенным данным
|
||||||
- **Redis кэш**: распределенное кэширование для множественных реплик
|
|
||||||
|
|
||||||
## Расписание обновлений
|
## Расписание обновлений
|
||||||
|
|
||||||
|
|
@ -83,12 +81,11 @@ wind, err := service.Extract(ctx, lat, lon, alt, timestamp)
|
||||||
- **Высокая производительность** (mmap, конкурентные загрузки)
|
- **Высокая производительность** (mmap, конкурентные загрузки)
|
||||||
- **Эффективное использование памяти** (не загружает весь массив в RAM)
|
- **Эффективное использование памяти** (не загружает весь массив в RAM)
|
||||||
- **Горизонтальное масштабирование** (stateless, множество реплик)
|
- **Горизонтальное масштабирование** (stateless, множество реплик)
|
||||||
- **Встроенное кэширование** (in-memory + Redis)
|
- **Встроенное кэширование** (in-memory)
|
||||||
|
|
||||||
### Особенности:
|
### Особенности:
|
||||||
- Использует `github.com/nilsmagnus/grib` вместо pygrib
|
- Использует `github.com/nilsmagnus/grib` вместо pygrib
|
||||||
- Реализует собственную логику интерполяции
|
- Реализует собственную логику интерполяции
|
||||||
- Поддерживает распределенные блокировки через Redis
|
|
||||||
|
|
||||||
## Конфигурация
|
## Конфигурация
|
||||||
|
|
||||||
|
|
@ -99,6 +96,5 @@ wind, err := service.Extract(ctx, lat, lon, alt, timestamp)
|
||||||
- `Dir` - директория для хранения файлов
|
- `Dir` - директория для хранения файлов
|
||||||
- `TTL` - время жизни данных (по умолчанию 24 часа)
|
- `TTL` - время жизни данных (по умолчанию 24 часа)
|
||||||
- `CacheTTL` - время жизни кэша (по умолчанию 1 час)
|
- `CacheTTL` - время жизни кэша (по умолчанию 1 час)
|
||||||
- `Redis` - Redis клиент для блокировок и кэша
|
|
||||||
- `Parallel` - количество параллельных загрузок
|
- `Parallel` - количество параллельных загрузок
|
||||||
- `Client` - HTTP клиент для загрузок
|
- `Client` - HTTP клиент для загрузок
|
||||||
|
|
@ -3,7 +3,6 @@ package grib
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -17,12 +16,6 @@ import (
|
||||||
"github.com/nilsmagnus/grib/griblib"
|
"github.com/nilsmagnus/grib/griblib"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RedisIface interface {
|
|
||||||
Lock(ctx context.Context, key string, ttl time.Duration) (func(context.Context), error)
|
|
||||||
Set(key string, value []byte, ttl time.Duration) error
|
|
||||||
Get(key string) ([]byte, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
Update(ctx context.Context) error
|
Update(ctx context.Context) error
|
||||||
Extract(ctx context.Context, lat, lon, alt float64, ts time.Time) ([2]float64, error)
|
Extract(ctx context.Context, lat, lon, alt float64, ts time.Time) ([2]float64, error)
|
||||||
|
|
@ -34,7 +27,6 @@ type ServiceConfig struct {
|
||||||
Dir string
|
Dir string
|
||||||
TTL time.Duration
|
TTL time.Duration
|
||||||
CacheTTL time.Duration
|
CacheTTL time.Duration
|
||||||
Redis RedisIface
|
|
||||||
Parallel int
|
Parallel int
|
||||||
Client *http.Client
|
Client *http.Client
|
||||||
DatasetURL string
|
DatasetURL string
|
||||||
|
|
@ -134,12 +126,6 @@ func (s *service) Update(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unlock, err := s.cfg.Redis.Lock(ctx, "grib-dl", 45*time.Minute)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer unlock(ctx)
|
|
||||||
|
|
||||||
// Check again after acquiring lock (double-checked locking pattern)
|
// Check again after acquiring lock (double-checked locking pattern)
|
||||||
if d := s.data.Load(); d != nil {
|
if d := s.data.Load(); d != nil {
|
||||||
runTime := time.Unix(d.runUTC, 0)
|
runTime := time.Unix(d.runUTC, 0)
|
||||||
|
|
@ -290,29 +276,6 @@ func (s *service) Extract(ctx context.Context, lat, lon, alt float64, ts time.Ti
|
||||||
return [2]float64(v), nil
|
return [2]float64(v), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try Redis cache
|
|
||||||
redisKey := fmt.Sprintf("grib:extract:%d", key)
|
|
||||||
if cached, err := s.cfg.Redis.Get(redisKey); err == nil {
|
|
||||||
var result [2]float64
|
|
||||||
if len(cached) == 16 {
|
|
||||||
result[0] = math.Float64frombits(binary.LittleEndian.Uint64(cached[:8]))
|
|
||||||
result[1] = math.Float64frombits(binary.LittleEndian.Uint64(cached[8:]))
|
|
||||||
s.cache.set(key, vec(result))
|
|
||||||
return result, nil
|
|
||||||
} else {
|
|
||||||
// Cache data is corrupted (wrong length)
|
|
||||||
return zero, errcodes.ErrRedisCacheCorrupted
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Check if it's a cache miss (expected error)
|
|
||||||
if errcode, ok := errcodes.AsErr(err); ok && errcode == errcodes.ErrRedisCacheMiss {
|
|
||||||
// Cache miss is expected, continue with calculation
|
|
||||||
} else {
|
|
||||||
// Unexpected error, return it
|
|
||||||
return zero, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate result
|
// Calculate result
|
||||||
td := ts.Sub(time.Unix(d.runUTC, 0)).Hours()
|
td := ts.Sub(time.Unix(d.runUTC, 0)).Hours()
|
||||||
u, v := d.uv(lat, lon, alt, td)
|
u, v := d.uv(lat, lon, alt, td)
|
||||||
|
|
@ -321,12 +284,6 @@ func (s *service) Extract(ctx context.Context, lat, lon, alt float64, ts time.Ti
|
||||||
// Cache in memory
|
// Cache in memory
|
||||||
s.cache.set(key, vec(out))
|
s.cache.set(key, vec(out))
|
||||||
|
|
||||||
// Cache in Redis
|
|
||||||
encoded := make([]byte, 16)
|
|
||||||
binary.LittleEndian.PutUint64(encoded[:8], math.Float64bits(out[0]))
|
|
||||||
binary.LittleEndian.PutUint64(encoded[8:], math.Float64bits(out[1]))
|
|
||||||
s.cfg.Redis.Set(redisKey, encoded, s.cfg.CacheTTL)
|
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,6 @@ type Config struct {
|
||||||
GribParallel int `env:"GSN_PREDICTOR_GRIB_PARALLEL" envDefault:"4"`
|
GribParallel int `env:"GSN_PREDICTOR_GRIB_PARALLEL" envDefault:"4"`
|
||||||
GribTimeout time.Duration `env:"GSN_PREDICTOR_GRIB_TIMEOUT" envDefault:"30s"`
|
GribTimeout time.Duration `env:"GSN_PREDICTOR_GRIB_TIMEOUT" envDefault:"30s"`
|
||||||
GribDatasetURL string `env:"GSN_PREDICTOR_GRIB_DATASET_URL" envDefault:"https://nomads.ncep.noaa.gov/pub/data/nccf/com/gfs/prod"`
|
GribDatasetURL string `env:"GSN_PREDICTOR_GRIB_DATASET_URL" envDefault:"https://nomads.ncep.noaa.gov/pub/data/nccf/com/gfs/prod"`
|
||||||
|
|
||||||
// --- Redis Configuration ---
|
|
||||||
RedisHost string `env:"GSN_PREDICTOR_REDIS_HOST"`
|
|
||||||
RedisPort int `env:"GSN_PREDICTOR_REDIS_PORT"`
|
|
||||||
RedisPassword string `env:"GSN_PREDICTOR_REDIS_PASSWORD"`
|
|
||||||
RedisDB int `env:"GSN_PREDICTOR_REDIS_DB"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) CreateHTTPClient() *http.Client {
|
func (c *Config) CreateHTTPClient() *http.Client {
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,3 @@ type Grib interface {
|
||||||
Extract(ctx context.Context, lat, lon, alt float64, ts time.Time) ([2]float64, error)
|
Extract(ctx context.Context, lat, lon, alt float64, ts time.Time) ([2]float64, error)
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
type Redis interface {
|
|
||||||
Lock(ctx context.Context, key string, ttl time.Duration) (func(context.Context), error)
|
|
||||||
Set(key string, value []byte, ttl time.Duration) error
|
|
||||||
Get(key string) ([]byte, error)
|
|
||||||
Close() error
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,31 @@ func (s *Service) customProfile(ctx context.Context, params ds.PredictionParamet
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rk4Step(lat, lon, alt float64, t time.Time, dt float64, windFunc func(lat, lon, alt float64, t time.Time) (float64, float64), altRate float64) (float64, float64, float64) {
|
||||||
|
// Helper for RK4 integration step
|
||||||
|
toRad := math.Pi / 180.0
|
||||||
|
toDeg := 180.0 / math.Pi
|
||||||
|
R := func(alt float64) float64 { return 6371009.0 + alt }
|
||||||
|
|
||||||
|
f := func(lat, lon, alt float64, t time.Time) (float64, float64, float64) {
|
||||||
|
windU, windV := windFunc(lat, lon, alt, t)
|
||||||
|
Rnow := R(alt)
|
||||||
|
dlat := toDeg * windV / Rnow
|
||||||
|
dlon := toDeg * windU / (Rnow * math.Cos(lat*toRad))
|
||||||
|
return dlat, dlon, altRate
|
||||||
|
}
|
||||||
|
|
||||||
|
k1_lat, k1_lon, k1_alt := f(lat, lon, alt, t)
|
||||||
|
k2_lat, k2_lon, k2_alt := f(lat+0.5*k1_lat*dt, lon+0.5*k1_lon*dt, alt+0.5*k1_alt*dt, t.Add(time.Duration(0.5*dt)*time.Second))
|
||||||
|
k3_lat, k3_lon, k3_alt := f(lat+0.5*k2_lat*dt, lon+0.5*k2_lon*dt, alt+0.5*k2_alt*dt, t.Add(time.Duration(0.5*dt)*time.Second))
|
||||||
|
k4_lat, k4_lon, k4_alt := f(lat+k3_lat*dt, lon+k3_lon*dt, alt+k3_alt*dt, t.Add(time.Duration(dt)*time.Second))
|
||||||
|
|
||||||
|
latNew := lat + (dt/6.0)*(k1_lat+2*k2_lat+2*k3_lat+k4_lat)
|
||||||
|
lonNew := lon + (dt/6.0)*(k1_lon+2*k2_lon+2*k3_lon+k4_lon)
|
||||||
|
altNew := alt + (dt/6.0)*(k1_alt+2*k2_alt+2*k3_alt+k4_alt)
|
||||||
|
return latNew, lonNew, altNew
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) simulateAscent(ctx context.Context, params ds.PredictionParameters, ascentRate, targetAltitude float64, customCurve *CustomCurve) []ds.PredicitonResult {
|
func (s *Service) simulateAscent(ctx context.Context, params ds.PredictionParameters, ascentRate, targetAltitude float64, customCurve *CustomCurve) []ds.PredicitonResult {
|
||||||
const dt = 10.0 // simulation step in seconds
|
const dt = 10.0 // simulation step in seconds
|
||||||
const outputInterval = 60.0 // output every 60 seconds
|
const outputInterval = 60.0 // output every 60 seconds
|
||||||
|
|
@ -230,7 +255,6 @@ func (s *Service) simulateAscent(ctx context.Context, params ds.PredictionParame
|
||||||
|
|
||||||
results := make([]ds.PredicitonResult, 0, 1000)
|
results := make([]ds.PredicitonResult, 0, 1000)
|
||||||
|
|
||||||
// Always include the initial launch point
|
|
||||||
latCopy := lat
|
latCopy := lat
|
||||||
lonCopy := lon
|
lonCopy := lon
|
||||||
altCopy := alt
|
altCopy := alt
|
||||||
|
|
@ -247,40 +271,39 @@ func (s *Service) simulateAscent(ctx context.Context, params ds.PredictionParame
|
||||||
WindV: &windV,
|
WindV: &windV,
|
||||||
})
|
})
|
||||||
|
|
||||||
var nextOutputTime = timeCur.Add(time.Duration(outputInterval) * time.Second)
|
nextOutputTime := timeCur.Add(time.Duration(outputInterval) * time.Second)
|
||||||
|
windFunc := func(lat, lon, alt float64, t time.Time) (float64, float64) {
|
||||||
for alt < targetAltitude {
|
w, err := s.ExtractWind(ctx, lat, lon, alt, t)
|
||||||
wind, err := s.ExtractWind(ctx, lat, lon, alt, timeCur)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Ctx(ctx).Warn("Wind extraction failed during ascent", zap.Error(err))
|
log.Ctx(ctx).Warn("Wind extraction failed during ascent", zap.Error(err))
|
||||||
break
|
return 0, 0
|
||||||
}
|
}
|
||||||
|
return w[0], w[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
for alt < targetAltitude {
|
||||||
altRate := ascentRate
|
altRate := ascentRate
|
||||||
if customCurve != nil {
|
if customCurve != nil {
|
||||||
altRate = s.getCustomAltitudeRate(customCurve, alt, ascentRate)
|
altRate = s.getCustomAltitudeRate(customCurve, alt, ascentRate)
|
||||||
}
|
}
|
||||||
|
latNew, lonNew, altNew := rk4Step(lat, lon, alt, timeCur, dt, windFunc, altRate)
|
||||||
latDot := (wind[1] / 111320.0)
|
|
||||||
lonDot := (wind[0] / (40075000.0 * math.Cos(lat*math.Pi/180) / 360.0))
|
|
||||||
|
|
||||||
lat += latDot * dt
|
|
||||||
lon += lonDot * dt
|
|
||||||
alt += altRate * dt
|
|
||||||
timeCur = timeCur.Add(time.Duration(dt) * time.Second)
|
timeCur = timeCur.Add(time.Duration(dt) * time.Second)
|
||||||
|
lat = latNew
|
||||||
|
lon = lonNew
|
||||||
|
alt = altNew
|
||||||
|
|
||||||
// Don't add a point if we've reached or exceeded target altitude
|
|
||||||
if alt >= targetAltitude {
|
if alt >= targetAltitude {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if !timeCur.Before(nextOutputTime) {
|
if !timeCur.Before(nextOutputTime) {
|
||||||
|
wU, wV := windFunc(lat, lon, alt, timeCur)
|
||||||
latCopy := lat
|
latCopy := lat
|
||||||
lonCopy := lon
|
lonCopy := lon
|
||||||
altCopy := alt
|
altCopy := alt
|
||||||
timeCopy := timeCur
|
timeCopy := timeCur
|
||||||
windU := wind[0]
|
windU := wU
|
||||||
windV := wind[1]
|
windV := wV
|
||||||
results = append(results, ds.PredicitonResult{
|
results = append(results, ds.PredicitonResult{
|
||||||
Latitude: &latCopy,
|
Latitude: &latCopy,
|
||||||
Longitude: &lonCopy,
|
Longitude: &lonCopy,
|
||||||
|
|
@ -307,7 +330,6 @@ func (s *Service) simulateDescent(ctx context.Context, params ds.PredictionParam
|
||||||
|
|
||||||
results := make([]ds.PredicitonResult, 0, 1000)
|
results := make([]ds.PredicitonResult, 0, 1000)
|
||||||
|
|
||||||
// Always include the initial descent point
|
|
||||||
latCopy := lat
|
latCopy := lat
|
||||||
lonCopy := lon
|
lonCopy := lon
|
||||||
altCopy := alt
|
altCopy := alt
|
||||||
|
|
@ -324,40 +346,39 @@ func (s *Service) simulateDescent(ctx context.Context, params ds.PredictionParam
|
||||||
WindV: &windV,
|
WindV: &windV,
|
||||||
})
|
})
|
||||||
|
|
||||||
var nextOutputTime = timeCur.Add(time.Duration(outputInterval) * time.Second)
|
nextOutputTime := timeCur.Add(time.Duration(outputInterval) * time.Second)
|
||||||
|
windFunc := func(lat, lon, alt float64, t time.Time) (float64, float64) {
|
||||||
for alt > targetAltitude {
|
w, err := s.ExtractWind(ctx, lat, lon, alt, t)
|
||||||
wind, err := s.ExtractWind(ctx, lat, lon, alt, timeCur)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Ctx(ctx).Warn("Wind extraction failed during descent", zap.Error(err))
|
log.Ctx(ctx).Warn("Wind extraction failed during descent", zap.Error(err))
|
||||||
break
|
return 0, 0
|
||||||
}
|
}
|
||||||
|
return w[0], w[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
for alt > targetAltitude {
|
||||||
altRate := -descentRate
|
altRate := -descentRate
|
||||||
if customCurve != nil {
|
if customCurve != nil {
|
||||||
altRate = -s.getCustomAltitudeRate(customCurve, alt, descentRate)
|
altRate = -s.getCustomAltitudeRate(customCurve, alt, descentRate)
|
||||||
}
|
}
|
||||||
|
latNew, lonNew, altNew := rk4Step(lat, lon, alt, timeCur, dt, windFunc, altRate)
|
||||||
latDot := (wind[1] / 111320.0)
|
|
||||||
lonDot := (wind[0] / (40075000.0 * math.Cos(lat*math.Pi/180) / 360.0))
|
|
||||||
|
|
||||||
lat += latDot * dt
|
|
||||||
lon += lonDot * dt
|
|
||||||
alt += altRate * dt
|
|
||||||
timeCur = timeCur.Add(time.Duration(dt) * time.Second)
|
timeCur = timeCur.Add(time.Duration(dt) * time.Second)
|
||||||
|
lat = latNew
|
||||||
|
lon = lonNew
|
||||||
|
alt = altNew
|
||||||
|
|
||||||
// Don't add a point if we've reached or gone below target altitude
|
|
||||||
if alt <= targetAltitude {
|
if alt <= targetAltitude {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if !timeCur.Before(nextOutputTime) {
|
if !timeCur.Before(nextOutputTime) {
|
||||||
|
wU, wV := windFunc(lat, lon, alt, timeCur)
|
||||||
latCopy := lat
|
latCopy := lat
|
||||||
lonCopy := lon
|
lonCopy := lon
|
||||||
altCopy := alt
|
altCopy := alt
|
||||||
timeCopy := timeCur
|
timeCopy := timeCur
|
||||||
windU := wind[0]
|
windU := wU
|
||||||
windV := wind[1]
|
windV := wV
|
||||||
results = append(results, ds.PredicitonResult{
|
results = append(results, ds.PredicitonResult{
|
||||||
Latitude: &latCopy,
|
Latitude: &latCopy,
|
||||||
Longitude: &lonCopy,
|
Longitude: &lonCopy,
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
cfg *Config
|
cfg *Config
|
||||||
redis Redis
|
grib Grib
|
||||||
grib Grib
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg *Config, gribService Grib, redisService Redis) (*Service, error) {
|
func New(cfg *Config, gribService Grib) (*Service, error) {
|
||||||
svc := &Service{
|
svc := &Service{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
redis: redisService,
|
grib: gribService,
|
||||||
grib: gribService,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return svc, nil
|
return svc, nil
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
package redis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Service defines the interface for Redis operations
|
|
||||||
type Service interface {
|
|
||||||
// Lock acquires a distributed lock
|
|
||||||
Lock(ctx context.Context, key string, ttl time.Duration) (func(context.Context), error)
|
|
||||||
|
|
||||||
// Set sets a key with value and TTL
|
|
||||||
Set(key string, value []byte, ttl time.Duration) error
|
|
||||||
|
|
||||||
// Get retrieves a value by key
|
|
||||||
Get(key string) ([]byte, error)
|
|
||||||
|
|
||||||
// Close closes the Redis connection
|
|
||||||
Close() error
|
|
||||||
}
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
package redis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.intra.yksa.space/gsn/predictor/internal/pkg/errcodes"
|
|
||||||
"github.com/bsm/redislock"
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
client *redis.Client
|
|
||||||
locker *redislock.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure Client implements Service interface
|
|
||||||
var _ Service = (*Client)(nil)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Host string `env:"HOST"`
|
|
||||||
Port int `env:"PORT"`
|
|
||||||
Password string `env:"PASSWORD"`
|
|
||||||
DB int `env:"DB"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(cfg Config) (*Client, error) {
|
|
||||||
client := redis.NewClient(&redis.Options{
|
|
||||||
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
|
|
||||||
Password: cfg.Password,
|
|
||||||
DB: cfg.DB,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Test connection
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if err := client.Ping(ctx).Err(); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to connect to redis: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
locker := redislock.New(client)
|
|
||||||
|
|
||||||
return &Client{
|
|
||||||
client: client,
|
|
||||||
locker: locker,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Lock(ctx context.Context, key string, ttl time.Duration) (func(context.Context), error) {
|
|
||||||
lock, err := c.locker.Obtain(ctx, key, ttl, nil)
|
|
||||||
if err != nil {
|
|
||||||
if err == redislock.ErrNotObtained {
|
|
||||||
return nil, errcodes.ErrRedisLockAlreadyLocked
|
|
||||||
}
|
|
||||||
return nil, errcodes.Wrap(err, "failed to obtain redis lock")
|
|
||||||
}
|
|
||||||
|
|
||||||
unlock := func(ctx context.Context) {
|
|
||||||
lock.Release(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
return unlock, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Set(key string, value []byte, ttl time.Duration) error {
|
|
||||||
ctx := context.Background()
|
|
||||||
if err := c.client.Set(ctx, key, value, ttl).Err(); err != nil {
|
|
||||||
return errcodes.Wrap(err, "failed to set redis key")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Get(key string) ([]byte, error) {
|
|
||||||
ctx := context.Background()
|
|
||||||
result := c.client.Get(ctx, key)
|
|
||||||
if result.Err() != nil {
|
|
||||||
if result.Err() == redis.Nil {
|
|
||||||
return nil, errcodes.ErrRedisCacheMiss
|
|
||||||
}
|
|
||||||
return nil, errcodes.Wrap(result.Err(), "failed to get redis key")
|
|
||||||
}
|
|
||||||
return result.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Close() error {
|
|
||||||
return c.client.Close()
|
|
||||||
}
|
|
||||||
|
|
@ -6,18 +6,16 @@ import requests
|
||||||
import json
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
import base64
|
import base64
|
||||||
|
import math
|
||||||
|
|
||||||
# --- Config ---
|
# --- Config ---
|
||||||
LOCAL_API_URL = "http://localhost:8080/api/v1/prediction"
|
REFERENCE_API_URL = "https://fly.stratonautica.ru/api/v2/?profile=standard_profile&pred_type=single&launch_datetime=2025-06-25T20%3A45%3A00Z&launch_latitude=56.6992&launch_longitude=38.8247&launch_altitude=0&ascent_rate=5&burst_altitude=30000&descent_rate=5"
|
||||||
REFERENCE_API_URL = (
|
LOCAL_API_URL = "http://localhost:8080/api/v1/prediction?profile=standard_profile&pred_type=single&launch_datetime=2025-06-25T20%3A45%3A00Z&launch_latitude=56.6992&launch_longitude=38.8247&launch_altitude=0&ascent_rate=5&burst_altitude=30000&descent_rate=5"
|
||||||
"https://fly.stratonautica.ru/api/v2/?profile=standard_profile&pred_type=single"
|
|
||||||
"&launch_datetime=2025-06-25T13:28:00Z&launch_latitude=56.6992&launch_longitude=38.8247"
|
|
||||||
"&launch_altitude=0&ascent_rate=5&burst_altitude=30000&descent_rate=5"
|
|
||||||
)
|
|
||||||
LOCAL_API_PAYLOAD = {
|
LOCAL_API_PAYLOAD = {
|
||||||
"launch_latitude": 56.6992,
|
"launch_latitude": 56.6992,
|
||||||
"launch_longitude": 38.8247,
|
"launch_longitude": 38.8247,
|
||||||
"launch_datetime": "2025-06-25T13:28:00Z",
|
"launch_datetime": "2025-06-25T20-45-000Z",
|
||||||
"launch_altitude": 0,
|
"launch_altitude": 0,
|
||||||
"profile": "standard_profile",
|
"profile": "standard_profile",
|
||||||
"ascent_rate": 5,
|
"ascent_rate": 5,
|
||||||
|
|
@ -68,18 +66,28 @@ def fetch_reference():
|
||||||
print(f"[INFO] Fetching reference prediction from {REFERENCE_API_URL}")
|
print(f"[INFO] Fetching reference prediction from {REFERENCE_API_URL}")
|
||||||
resp = requests.get(REFERENCE_API_URL, timeout=60)
|
resp = requests.get(REFERENCE_API_URL, timeout=60)
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
print(f"[ERROR] Reference API returned {resp.status_code}")
|
print(f"[ERROR] Reference API returned {resp.status_code}: {resp.text}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return resp.json()
|
return resp.json()
|
||||||
|
|
||||||
def fetch_local():
|
def fetch_local():
|
||||||
print(f"[INFO] Fetching local prediction from {LOCAL_API_URL}")
|
print(f"[INFO] Fetching local prediction from {LOCAL_API_URL}")
|
||||||
resp = requests.post(LOCAL_API_URL, json=LOCAL_API_PAYLOAD, timeout=120)
|
resp = requests.get(LOCAL_API_URL, timeout=60)
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
print(f"[ERROR] Local API returned {resp.status_code}: {resp.text}")
|
print(f"[ERROR] Local API returned {resp.status_code}: {resp.text}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return resp.json()
|
return resp.json()
|
||||||
|
|
||||||
|
def haversine(lat1, lon1, lat2, lon2):
|
||||||
|
"""Calculate the great-circle distance between two points on the Earth (specified in decimal degrees). Returns distance in kilometers."""
|
||||||
|
R = 6371.0 # Earth radius in kilometers
|
||||||
|
lat1, lon1, lat2, lon2 = map(math.radians, [lat1, lon1, lat2, lon2])
|
||||||
|
dlat = lat2 - lat1
|
||||||
|
dlon = lon2 - lon1
|
||||||
|
a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2
|
||||||
|
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
||||||
|
return R * c
|
||||||
|
|
||||||
def compare_results(reference_data, local_data):
|
def compare_results(reference_data, local_data):
|
||||||
"""Compare prediction results between reference and local APIs."""
|
"""Compare prediction results between reference and local APIs."""
|
||||||
print("[INFO] Comparing results ...")
|
print("[INFO] Comparing results ...")
|
||||||
|
|
@ -116,38 +124,58 @@ def compare_results(reference_data, local_data):
|
||||||
print(f"[DIFF] Trajectory length mismatch: {len(local_trajectory)} vs {len(ref_trajectory)}")
|
print(f"[DIFF] Trajectory length mismatch: {len(local_trajectory)} vs {len(ref_trajectory)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Compare trajectory points
|
# Compare trajectory points and calculate drift
|
||||||
min_len = min(len(ref_trajectory), len(local_trajectory))
|
min_len = min(len(ref_trajectory), len(local_trajectory))
|
||||||
|
max_drift = 0.0
|
||||||
|
max_drift_idx = -1
|
||||||
|
drift_list = []
|
||||||
|
print("\n[DRIFT] Trajectory point-by-point distance (km):")
|
||||||
|
for i in range(min_len):
|
||||||
|
ref_point = ref_trajectory[i]
|
||||||
|
local_point = local_trajectory[i]
|
||||||
|
ref_lat = ref_point.get('latitude')
|
||||||
|
ref_lon = ref_point.get('longitude')
|
||||||
|
local_lat = local_point.get('latitude')
|
||||||
|
local_lon = local_point.get('longitude')
|
||||||
|
drift_km = None
|
||||||
|
if None not in (ref_lat, ref_lon, local_lat, local_lon):
|
||||||
|
drift_km = haversine(ref_lat, ref_lon, local_lat, local_lon)
|
||||||
|
drift_list.append(drift_km)
|
||||||
|
if drift_km > max_drift:
|
||||||
|
max_drift = drift_km
|
||||||
|
max_drift_idx = i
|
||||||
|
print(f" [{i}] Drift: {drift_km:.3f} km")
|
||||||
|
else:
|
||||||
|
print(f" [{i}] Drift: N/A (missing lat/lon)")
|
||||||
|
if drift_list:
|
||||||
|
mean_drift = sum(drift_list) / len(drift_list)
|
||||||
|
print(f"\n[DRIFT] Max drift: {max_drift:.3f} km at idx {max_drift_idx}")
|
||||||
|
print(f"[DRIFT] Mean drift: {mean_drift:.3f} km over {len(drift_list)} points")
|
||||||
|
else:
|
||||||
|
print("[DRIFT] No valid drift data to report.")
|
||||||
|
# Continue with original comparison for altitude, etc.
|
||||||
for i in range(min_len):
|
for i in range(min_len):
|
||||||
ref_point = ref_trajectory[i]
|
ref_point = ref_trajectory[i]
|
||||||
local_point = local_trajectory[i]
|
local_point = local_trajectory[i]
|
||||||
|
|
||||||
# Compare key fields
|
|
||||||
for key in ['altitude', 'latitude', 'longitude']:
|
for key in ['altitude', 'latitude', 'longitude']:
|
||||||
ref_val = ref_point.get(key)
|
ref_val = ref_point.get(key)
|
||||||
local_val = local_point.get(key)
|
local_val = local_point.get(key)
|
||||||
|
|
||||||
if ref_val is not None and local_val is not None:
|
if ref_val is not None and local_val is not None:
|
||||||
# Use relative tolerance for floating point comparison
|
if abs(ref_val - local_val) > 0.1:
|
||||||
if abs(ref_val - local_val) > 0.1: # 0.1 degree/meter tolerance
|
|
||||||
print(f"[DIFF] At idx {i}, key {key}: {local_val} != {ref_val}")
|
print(f"[DIFF] At idx {i}, key {key}: {local_val} != {ref_val}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
print("[SUCCESS] Results match!")
|
print("[SUCCESS] Results match!")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def test_custom_profile():
|
def test_custom_profile():
|
||||||
"""Test custom profile with base64 encoded curve."""
|
"""Test custom profile with base64 encoded curve."""
|
||||||
print("\n[TEST] Testing custom_profile...")
|
print("\n[TEST] Testing custom_profile...")
|
||||||
|
|
||||||
# Create a simple custom ascent curve (altitude vs time in seconds)
|
# Create a simple custom ascent curve (altitude vs time in seconds)
|
||||||
curve_data = {
|
curve_data = {
|
||||||
"altitude": [0, 30000],
|
"altitude": [0, 30000],
|
||||||
"time": [0, 6000]
|
"time": [0, 6000]
|
||||||
}
|
}
|
||||||
|
|
||||||
curve_b64 = base64.b64encode(json.dumps(curve_data).encode()).decode()
|
curve_b64 = base64.b64encode(json.dumps(curve_data).encode()).decode()
|
||||||
|
|
||||||
# Test parameters for custom profile
|
# Test parameters for custom profile
|
||||||
params = {
|
params = {
|
||||||
"launch_latitude": 56.6992,
|
"launch_latitude": 56.6992,
|
||||||
|
|
@ -157,17 +185,15 @@ def test_custom_profile():
|
||||||
"profile": "custom_profile",
|
"profile": "custom_profile",
|
||||||
"ascent_curve": curve_b64
|
"ascent_curve": curve_b64
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Test local API
|
# Test local API (use GET)
|
||||||
local_resp = requests.post(
|
local_resp = requests.get(
|
||||||
"http://localhost:8080/api/v1/prediction",
|
"http://localhost:8080/api/v1/prediction",
|
||||||
json=params,
|
params=params,
|
||||||
timeout=30
|
timeout=30
|
||||||
)
|
)
|
||||||
local_resp.raise_for_status()
|
local_resp.raise_for_status()
|
||||||
local_data = local_resp.json()
|
local_data = local_resp.json()
|
||||||
|
|
||||||
print(f"[INFO] Custom profile test - Local API returned {len(local_data.get('prediction', [{}])[0].get('trajectory', []))} trajectory points")
|
print(f"[INFO] Custom profile test - Local API returned {len(local_data.get('prediction', [{}])[0].get('trajectory', []))} trajectory points")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -223,21 +249,18 @@ def test_single_profile(profile):
|
||||||
"burst_altitude": 30000,
|
"burst_altitude": 30000,
|
||||||
"descent_rate": 5
|
"descent_rate": 5
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add float altitude for float profile
|
# Add float altitude for float profile
|
||||||
if profile == "float_profile":
|
if profile == "float_profile":
|
||||||
params["float_altitude"] = 25000
|
params["float_altitude"] = 25000
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Test local API
|
# Test local API (use GET)
|
||||||
local_resp = requests.post(
|
local_resp = requests.get(
|
||||||
"http://localhost:8080/api/v1/prediction",
|
"http://localhost:8080/api/v1/prediction",
|
||||||
json=params,
|
params=params,
|
||||||
timeout=30
|
timeout=30
|
||||||
)
|
)
|
||||||
local_resp.raise_for_status()
|
local_resp.raise_for_status()
|
||||||
local_data = local_resp.json()
|
local_data = local_resp.json()
|
||||||
|
|
||||||
print(f"[INFO] {profile} - Local API returned {len(local_data.get('prediction', [{}])[0].get('trajectory', []))} trajectory points")
|
print(f"[INFO] {profile} - Local API returned {len(local_data.get('prediction', [{}])[0].get('trajectory', []))} trajectory points")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue